diff --git a/.bzrignore b/.bzrignore index d133e1f64..ce62572e0 100644 --- a/.bzrignore +++ b/.bzrignore @@ -5,6 +5,7 @@ django-nova/downloads/ django-nova/eggs/ django-nova/parts/ django-nova/src/django_nova.egg-info +django-nova-syspanel/src/django_nova_syspanel.egg-info openstack-dashboard/.dashboard-venv openstack-dashboard/local/dashboard_openstack.sqlite3 openstack-dashboard/local/local_settings.py diff --git a/django-nova-syspanel/LICENSE b/django-nova-syspanel/LICENSE new file mode 100644 index 000000000..68c771a09 --- /dev/null +++ b/django-nova-syspanel/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/django-nova-syspanel/README b/django-nova-syspanel/README new file mode 100644 index 000000000..fbd9a8dd6 --- /dev/null +++ b/django-nova-syspanel/README @@ -0,0 +1,38 @@ +OpenStack Django-Nova-Syspanel +------------------------------ + +The Django-Nova-Syspanel package provides a django app that interacts +with the OpenStack Nova cloud controller to view the health of a +running OpenStack installation. + +This is packaged, and intended to be used inside of, OpenStack +Dashboard, which is located at + + http://launchpad.net/openstack-dashboard + +Development against this application is best done inside this reference +implementation, though in practice any django app can install this +application. + + +Getting Started +--------------- + +Django-Nova-Syspanel uses Buildout (http://www.buildout.org/) to +manage local development. To configure your local Buildout +environment: + + $ python bootstrap.py + $ bin/buildout + +This will install all the dependencies of django-nova-syspanel and +provide some useful scripts in the bin/ directory: + + bin/python provides a python shell for the current buildout. + bin/django provides django functions for the current buildout. + + +You should now be able to run unit tests as follows: + + $ bin/django test + diff --git a/django-nova-syspanel/bootstrap.py b/django-nova-syspanel/bootstrap.py new file mode 100644 index 000000000..5f2cb0835 --- /dev/null +++ b/django-nova-syspanel/bootstrap.py @@ -0,0 +1,260 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess +from optparse import OptionParser + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + quote = str + +# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. +stdout, stderr = subprocess.Popen( + [sys.executable, '-Sc', + 'try:\n' + ' import ConfigParser\n' + 'except ImportError:\n' + ' print 1\n' + 'else:\n' + ' print 0\n'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() +has_broken_dash_S = bool(int(stdout.strip())) + +# In order to be more robust in the face of system Pythons, we want to +# run without site-packages loaded. This is somewhat tricky, in +# particular because Python 2.6's distutils imports site, so starting +# with the -S flag is not sufficient. However, we'll start with that: +if not has_broken_dash_S and 'site' in sys.modules: + # We will restart with python -S. + args = sys.argv[:] + args[0:0] = [sys.executable, '-S'] + args = map(quote, args) + os.execv(sys.executable, args) +# Now we are running with -S. We'll get the clean sys.path, import site +# because distutils will do it later, and then reset the path and clean +# out any namespace packages from site-packages that might have been +# loaded by .pth files. +clean_path = sys.path[:] +import site +sys.path[:] = clean_path +for k, v in sys.modules.items(): + if k in ('setuptools', 'pkg_resources') or ( + hasattr(v, '__path__') and + len(v.__path__)==1 and + not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))): + # This is a namespace package. Remove it. + sys.modules.pop(k) + +is_jython = sys.platform.startswith('java') + +setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' +distribute_source = 'http://python-distribute.org/distribute_setup.py' + +# parsing arguments +def normalize_to_url(option, opt_str, value, parser): + if value: + if '://' not in value: # It doesn't smell like a URL. + value = 'file://%s' % ( + urllib.pathname2url( + os.path.abspath(os.path.expanduser(value))),) + if opt_str == '--download-base' and not value.endswith('/'): + # Download base needs a trailing slash to make the world happy. + value += '/' + else: + value = None + name = opt_str[2:].replace('-', '_') + setattr(parser.values, name, value) + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --setup-source and --download-base to point to +local resources, you can keep this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="use_distribute", default=False, + help="Use Distribute rather than Setuptools.") +parser.add_option("--setup-source", action="callback", dest="setup_source", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or file location for the setup file. " + "If you use Setuptools, this will default to " + + setuptools_source + "; if you use Distribute, this " + "will default to " + distribute_source +".")) +parser.add_option("--download-base", action="callback", dest="download_base", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or directory for downloading " + "zc.buildout and either Setuptools or Distribute. " + "Defaults to PyPI.")) +parser.add_option("--eggs", + help=("Specify a directory for storing eggs. Defaults to " + "a temporary directory that is deleted when the " + "bootstrap script completes.")) +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", None, action="store", dest="config_file", + help=("Specify the path to the buildout configuration " + "file to be used.")) + +options, args = parser.parse_args() + +# if -c was provided, we push it back into args for buildout's main function +if options.config_file is not None: + args += ['-c', options.config_file] + +if options.eggs: + eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) +else: + eggs_dir = tempfile.mkdtemp() + +if options.setup_source is None: + if options.use_distribute: + options.setup_source = distribute_source + else: + options.setup_source = setuptools_source + +if options.accept_buildout_test_releases: + args.append('buildout:accept-buildout-test-releases=true') +args.append('bootstrap') + +try: + import pkg_resources + import setuptools # A flag. Sometimes pkg_resources is installed alone. + if not hasattr(pkg_resources, '_distribute'): + raise ImportError +except ImportError: + ez_code = urllib2.urlopen( + options.setup_source).read().replace('\r\n', '\n') + ez = {} + exec ez_code in ez + setup_args = dict(to_dir=eggs_dir, download_delay=0) + if options.download_base: + setup_args['download_base'] = options.download_base + if options.use_distribute: + setup_args['no_fake'] = True + ez['use_setuptools'](**setup_args) + if 'pkg_resources' in sys.modules: + reload(sys.modules['pkg_resources']) + import pkg_resources + # This does not (always?) update the default working set. We will + # do it. + for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +cmd = [quote(sys.executable), + '-c', + quote('from setuptools.command.easy_install import main; main()'), + '-mqNxd', + quote(eggs_dir)] + +if not has_broken_dash_S: + cmd.insert(1, '-S') + +find_links = options.download_base +if not find_links: + find_links = os.environ.get('bootstrap-testing-find-links') +if find_links: + cmd.extend(['-f', quote(find_links)]) + +if options.use_distribute: + setup_requirement = 'distribute' +else: + setup_requirement = 'setuptools' +ws = pkg_resources.working_set +setup_requirement_path = ws.find( + pkg_resources.Requirement.parse(setup_requirement)).location +env = dict( + os.environ, + PYTHONPATH=setup_requirement_path) + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setup_requirement_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +if is_jython: + import subprocess + exitcode = subprocess.Popen(cmd, env=env).wait() +else: # Windows prefers this, apparently; otherwise we would prefer subprocess + exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) +if exitcode != 0: + sys.stdout.flush() + sys.stderr.flush() + print ("An error occurred when trying to install zc.buildout. " + "Look above this message for any errors that " + "were output by easy_install.") + sys.exit(exitcode) + +ws.add_entry(eggs_dir) +ws.require(requirement) +import zc.buildout.buildout +zc.buildout.buildout.main(args) +if not options.eggs: # clean up temporary egg directory + shutil.rmtree(eggs_dir) diff --git a/django-nova-syspanel/buildout.cfg b/django-nova-syspanel/buildout.cfg new file mode 100644 index 000000000..c129890b5 --- /dev/null +++ b/django-nova-syspanel/buildout.cfg @@ -0,0 +1,19 @@ +[buildout] +parts = python django +develop = . +eggs = django-nova-syspanel + +[python] +recipe = zc.recipe.egg +interpreter = python +eggs = ${buildout:eggs} + +[django] +recipe = djangorecipe +version = 1.2.4 +project = django_nova_syspanel +projectegg = django_nova_syspanel +settings = testsettings +test = django_nova_syspanel +eggs = ${buildout:eggs} + diff --git a/django-nova-syspanel/setup.py b/django-nova-syspanel/setup.py new file mode 100644 index 000000000..bca22b3d0 --- /dev/null +++ b/django-nova-syspanel/setup.py @@ -0,0 +1,29 @@ +import os +from setuptools import setup, find_packages + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "django-nova-syspanel", + version = "0.1", + url = 'https://launchpad.net/openstack-dashboard/', + license = 'Apache 2.0', + description = "A Django interface for OpenStack Nova.", + long_description = read('README'), + author = 'Todd Willey', + author_email = 'xtoddx@gmail.com', + packages = find_packages('src'), + package_dir = {'': 'src'}, + install_requires = ['setuptools', 'boto==1.9b', 'mox>=0.5.0'], + classifiers = [ + 'Development Status :: 4 - Beta', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + ] +) + diff --git a/django-nova-syspanel/src/django_nova_syspanel/__init__.py b/django-nova-syspanel/src/django_nova_syspanel/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django-nova-syspanel/src/django_nova_syspanel/forms.py b/django-nova-syspanel/src/django_nova_syspanel/forms.py new file mode 100644 index 000000000..d57488e0c --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/forms.py @@ -0,0 +1,9 @@ +from django import forms + + +class DisableProject(forms.Form): + project_name = forms.CharField() + + +class DisableIpAddress(forms.Form): + cidr = forms.CharField() diff --git a/django-nova-syspanel/src/django_nova_syspanel/models.py b/django-nova-syspanel/src/django_nova_syspanel/models.py new file mode 100644 index 000000000..209beaf07 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/models.py @@ -0,0 +1,418 @@ +import sys +import boto +import boto.exception +import boto.s3 +from boto.ec2.volume import Volume + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.db.models.signals import post_save +from django.shortcuts import render_to_response +from django.template import RequestContext +from xml.dom import minidom +from nova_adminclient import NovaAdminClient + +project_permissions = ('admin',) + +class NovaResponseError(Exception): + def __init__(self, ec2error): + if ec2error.reason == 'Forbidden': + self.code = 'Forbidden' + self.message = 'Forbidden' + return + + dom = minidom.parseString(ec2error.body) + err = dom.getElementsByTagName('Errors')[0] + + try: + self.code = err.getElementsByTagName('Code')[0].childNodes[0].data + except IndexError: + self.code = 'Unknown' + + try: + self.message = err.getElementsByTagName('Message')[0].childNodes[0].data + except IndexError: + self.message = 'An unexpected error occurred. Please try your request again.' + + dom.unlink() + + def __str__(self): + return '%s (%s)' % (self.message, self.code) + + +def handle_nova_error(fn): + def decorator(view_func): + def wrapper(*args, **kwargs): + try: + return view_func(*args, **kwargs) + except boto.exception.EC2ResponseError, e: + if e.reason == 'Forbidden': + raise PermissionDenied() + else: + raise NovaResponseError(e) + return wrapper + return decorator(fn) + +def get_nova_admin_connection(): + """ + Returns a Nova administration connection. + """ + return NovaAdminClient ( + clc_url=settings.NOVA_DEFAULT_ENDPOINT, + region=settings.NOVA_DEFAULT_REGION, + access_key=settings.NOVA_ACCESS_KEY, + secret_key=settings.NOVA_SECRET_KEY + ) + +class VolumeInfo(object): + """Foo""" + + def __init__(self, connection=None, username=None, endpoint=None): + print "VOLUME" + print "==================================" + print "==================================" + print "==================================" + self.connection = connection + self.username = username + self.endpoint = endpoint + + self.id = None + self.create_time = None + self.status = None + self.size = None + self.snapshot_id = None + self.attach_data = None + self.zone = None + + self.display_name = None + self.display_description = None + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'volumeId': + self.id = str(value) + elif name == 'size': + self.size = int(value) + elif name == 'status': + self.status = str(value) + elif name == 'displayName': + self.display_name = str(value) + elif name == 'displayDescription': + self.display_description = str(value) + else: + print "*** UNKNOWN *** %s ***" % name + setattr(self, name, str(value)) + print repr(self) + +class ProjectManager(object): + def __init__(self, username, project, region): + self.username = username + self.projectname = project.projectname + self.projectManagerId = project.projectManagerId + self.region = region + + def get_nova_connection(self): + """ + Returns a boto connection for a user's project. + """ + nova = get_nova_admin_connection() + return nova.connection_for(self.username, + self.projectname, + clc_url=self.region['endpoint'], + region=self.region['name']) + + def get_zip(self): + """ + Returns a buffer of a zip file containing signed credentials + for the project's Nova user. + """ + nova = get_nova_admin_connection() + return nova.get_zip(self.username, self.projectname) + + def get_images(self, image_ids=None): + conn = self.get_nova_connection() + images = conn.get_all_images(image_ids=image_ids) + sorted_images = [i for i in images if i.ownerId == self.username] + \ + [i for i in images if i.ownerId != self.username] + + return [i for i in sorted_images if i.type == 'machine' and i.location.split('/')[0] != 'nova'] + + def get_image(self, image_id): + try: + return self.get_images(image_ids=[image_id,])[0] + except IndexError: + return None + + @handle_nova_error + def deregister_image(self, image_id): + """ + Removes the image's listing but leaves the image + and manifest in the object store in tact. + """ + conn = self.get_nova_connection() + return conn.deregister_image(image_id) + + @handle_nova_error + def update_image(self, image_id, display_name=None, description=None): + conn = self.get_nova_connection() + params = { + 'ImageId': image_id, + 'DisplayName': display_name, + 'Description': description + } + return conn.get_object('UpdateImage', params, boto.ec2.image.Image) + + @handle_nova_error + def run_instances(self, image_id, **kwargs): + """ + Runs instances of the specified image id. + """ + conn = self.get_nova_connection() + return conn.run_instances(image_id, **kwargs) + + def get_instance_count(self): + """ + Returns the number of active instances in this project or None if unknown. + """ + try: + return len(self.get_instances()) + except: + return None + + @handle_nova_error + def get_instances(self): + """ + Returns all instances in this project. + """ + conn = self.get_nova_connection() + reservations = conn.get_all_instances() + instances = [] + for reservation in reservations: + for instance in reservation.instances: + instances.append(instance) + return instances + + @handle_nova_error + def get_instance(self, instance_id): + """ + Returns detail about the specified instance. + """ + conn = self.get_nova_connection() + # TODO: Refactor this once nova's describe_instances filters by instance_id. + reservations = conn.get_all_instances() + for reservation in reservations: + for instance in reservation.instances: + if instance.id == instance_id: + return instance + return None + + @handle_nova_error + def update_instance(self, instance_id, updates): + conn = self.get_nova_connection() + params = {'InstanceId': instance_id, 'DisplayName': updates['nickname'], + 'DisplayDescription': updates['description']} + return conn.get_object('UpdateInstance', params, + boto.ec2.instance.Instance) + + def get_instance_graph(self, region, instance_id, graph_name): + # TODO(devcamcar): Need better support for multiple regions. + # Need a way to get object store by region. + s3 = boto.s3.connection.S3Connection ( + aws_access_key_id=settings.NOVA_ACCESS_KEY, + aws_secret_access_key=settings.NOVA_SECRET_KEY, + is_secure=False, + calling_format=boto.s3.connection.OrdinaryCallingFormat(), + port=3333, + host=settings.NOVA_CLC_IP + ) + key = '_%s.monitor' % instance_id + + try: + bucket = s3.get_bucket(key, validate=False) + except S3ResponseError, e: + if e.code == "NoSuchBucket": + return None + else: + raise e + + key = bucket.get_key(graph_name) + + return key.read() + + @handle_nova_error + def terminate_instance(self, instance_id): + """ Terminates the specified instance within this project. """ + conn = self.get_nova_connection() + conn.terminate_instances([instance_id]) + + @handle_nova_error + def get_security_groups(self): + """ + Returns all security groups associated with this project. + """ + conn = self.get_nova_connection() + groups = [] + + for g in conn.get_all_security_groups(): + # Do not show vpn group. + #if g.name != 'vpn-secgroup': + groups.append(g) + + return groups + + @handle_nova_error + def get_security_group(self, name): + """ + Returns the specified security group for this project. + """ + conn = self.get_nova_connection() + + try: + return conn.get_all_security_groups(groupnames=name.encode('ASCII'))[0] + except IndexError: + return None + + @handle_nova_error + def has_security_group(self, name): + """ + Indicates whether a security group with the specified name exists in this project. + """ + return self.get_security_group(name) != None + + @handle_nova_error + def create_security_group(self, name, description): + """ + Creates a new security group in this project. + """ + conn = self.get_nova_connection() + return conn.create_security_group(name, description) + + @handle_nova_error + def delete_security_group(self, name): + """ + Deletes a security group from the project. + """ + conn = self.get_nova_connection() + return conn.delete_security_group(name = name) + + @handle_nova_error + def authorize_security_group(self, group_name, ip_protocol, from_port, to_port): + """ + Authorizes a rule for the specified security group. + """ + conn = self.get_nova_connection() + return conn.authorize_security_group ( + group_name = group_name, + ip_protocol = ip_protocol, + from_port = from_port, + to_port = to_port, + cidr_ip = '0.0.0.0/0' + ) + + @handle_nova_error + def revoke_security_group(self, group_name, ip_protocol, from_port, to_port): + """ + Revokes a rule for the specified security group. + """ + conn = self.get_nova_connection() + return conn.revoke_security_group ( + group_name = group_name, + ip_protocol = ip_protocol, + from_port = from_port, + to_port = to_port, + cidr_ip = '0.0.0.0/0' + ) + + @handle_nova_error + def get_key_pairs(self): + """ + Returns all key pairs associated with this project. + """ + conn = self.get_nova_connection() + keys = [] + + for k in conn.get_all_key_pairs(): + # Do not show vpn key. + if k.name != 'vpn-key': + keys.append(k) + + return keys + + @handle_nova_error + def get_key_pair(self, name): + """ + Returns the specified security group for this project. + """ + conn = self.get_nova_connection() + + try: + return conn.get_all_key_pairs(keynames=name.encode('ASCII'))[0] + except IndexError: + return None + + @handle_nova_error + def has_key_pair(self, name): + """ + Indicates whether a key pair with the specified name exists in this project. + """ + return self.get_key_pair(name) != None + + @handle_nova_error + def create_key_pair(self, name): + """ + Creates a new key pair for this project. + """ + conn = self.get_nova_connection() + return conn.create_key_pair(name) + + @handle_nova_error + def delete_key_pair(self, name): + """ + Deletes a new key pair from this project. + """ + conn = self.get_nova_connection() + conn.delete_key_pair(name) + + @handle_nova_error + def get_volumes(self): + """ + Returns all volumes in this project. + """ + conn = self.get_nova_connection() + return conn.get_all_volumes() + + @handle_nova_error + def create_volume(self, size, display_name=None, display_description=None, + snapshot=None): + conn = self.get_nova_connection() + params = {'Size': size, 'DisplayName': display_name, + 'DisplayDescription': display_description} + return conn.get_object('CreateVolume', params, boto.ec2.volume.Volume) + + @handle_nova_error + def delete_volume(self, volume_id): + conn = self.get_nova_connection() + return conn.delete_volume(volume_id) + + @handle_nova_error + def attach_volume(self, volume_id, instance_id, device): + conn = self.get_nova_connection() + return conn.attach_volume(volume_id, instance_id, device) + + @handle_nova_error + def detach_volume(self, volume_id): + conn = self.get_nova_connection() + return conn.detach_volume(volume_id) + +def user_post_save(sender, instance, created, *args, **kwargs): + """ + Creates a Nova User when a new Django User is created. + """ + if created: + nova = get_nova_admin_connection() + if not nova.has_user(instance.username): + nova.create_user(instance.username) +post_save.connect(user_post_save, User, dispatch_uid='django.contrib.auth.models.User.post_save') diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/cloudview/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/cloudview/index.html new file mode 100644 index 000000000..dcdfda502 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/cloudview/index.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block breadcrumb %}{{ block.super }}
  • Cloud View
  • {% endblock %} + +{% block content %} +
    +

    Cloud View

    +

    View servers that participate in your cloud. See statistics about + running services and served objects.

    +
    + + + + + + + + + + + + {% for node in nodes %} + + + {% if node.compute %} + + + {% else %} + + + {% endif %} + + {% if node.volume %} + + + {% else %} + + + {% endif %} + + + {% endfor %} +
    HostnameCompute StatusInstance CountVolume StatusVolume Count
    {{node.hostname}}{{node.compute}}{{node.instance_count}}N/AN/A{{node.volume}}{{node.volume_count}}N/AN/A
    + +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/index.html new file mode 100644 index 000000000..ef42fdfae --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/index.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block content %} +

    NASA Nebula System Panel

    + +
    +

    Manage Instances

    +

    View all running instances other than VPNs. Schedule instances for + reboot or termination. Download logs.

    + Manage Instances → +
    + +
    +

    Manage Volumes

    +

    View all volumes. See size, status, and mount point. Destroy volumes.

    + Manage Volumes → +
    + +
    +

    Manage VPN

    +

    View all projects VPN status. Schedule launch, termination, and reboot. + Send credentials.

    + Manage VPN → +
    + +
    +

    Cloud View

    +

    View servers that participate in your cloud. See statistics about + running services and served objects.

    + Cloud View → +
    + +
    +

    Security

    +

    Perform operations to deal with rogue users, limit or prevent compromise, + and deal with external threats.

    + Security → +
    + +{#
    #} +{#

    Resource Monitors

    #} +{#

    Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec venenatis, libero ac auctor laoreet, nunc dolor volutpat purus, sit amet posuere turpis diam sit amet tellus. Praesent nec pede.

    #} +{# Resource Monitors → #} +{#
    #} +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/instances/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/instances/index.html new file mode 100644 index 000000000..ac1f50358 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/instances/index.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block headerjs %} + + + + + +{% endblock %} + +{% block breadcrumb %}{{ block.super }}
  • Instances
  • {% endblock %} + +{% block content %} +
    +

    Manage Instances

    +

    View all running instances other than VPNs. Schedule instances for + reboot or termination. Download logs.

    +
    +
    + {% if status == "success" %} +
    {{message}}
    + {% endif %} + + {% if status == "error" %} +
    {{message}}
    + {% endif %} +
    +
    + Refresh List +
    + + + + + + + + + + + + + + {% for instance in instances %} + + + + + + + + + + + + + + {% endfor %} + +
    IdUserNodeCreatedImageIPPublic IPVolumesStateOperations
    {{instance.id}}{{instance.owner_id}}{{instance.host_name}}{{instance.launch_time}}{{instance.image_id}}{{instance.public_dns_name}}{{instance.private_ip_address}}vol-12324{{instance.state}} + Terminate | + Restart | + Nova Log | + Console Log +
    +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/disable_project_credentials.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/disable_project_credentials.html new file mode 100644 index 000000000..694300723 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/disable_project_credentials.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block breadcrumb %}{{ block.super }}
  • Security
  • {% endblock %} + +{% block content %} +

    {{project}} Disabled

    + +

    + If a user's credentials were comprimised, you will need to use + `nova-manage user revoke USERNAME`, and the restart all VPNs for projects + they are a member of. +

    + +

    + The VPN for {{project}} is currently offline. You can turn it back on + using VPN overview. +

    + +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/index.html new file mode 100644 index 000000000..f9e3de358 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/security/index.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} + +{% block breadcrumb %}{{ block.super }}
  • Security
  • {% endblock %} + +{% block content %} +

    Security

    + +

    Use the following tools to secure the cloud during a security event

    + +
    +
    +
    +

    Scenario: Project account credentials have been compromised.

    +

    Action: Disable the user or project. This should invalidate the credentials, also deny the specific port assigned to the user.

    +
    + +
    + {% csrf_token %} +
    + {% for field in project_form %} + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} + {% endfor %} + + +
    +
    +
    + +
    +
    +

    Scenario: The issue pin points to be coming in from a specific external ip or ip range.

    +

    Action: Block the IP or IP range. This should manipulate ip tables to block the ip or ip range.

    +
    + +
    + {% csrf_token %} +
    + {% for field in ip_form %} + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} + {% endfor %} + + +
    +
    +
    +
    + +
    +
    +

    Scenario: Issues affecting multiple public entry points or could not determine to be a specific one.

    +

    Action: Disable all public IPs on all VMs. This should unplumb the device the public ip was on.

    +
    + +
    + {% csrf_token %} +
    + +
    +
    +
    + +
    +
    +

    Scenario: Issue related to VPN service affecting multiple customers.

    +

    Action: Disable VPN service. This should turn off the VPN ip.

    +
    + +
    + {% csrf_token %} +
    + +
    +
    +
    + +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/volumes/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/volumes/index.html new file mode 100644 index 000000000..b2aa2b602 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/volumes/index.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block headerjs %} + + + + + +{% endblock %} + +{% block breadcrumb %}{{ block.super }}
  • Volumes
  • {% endblock %} + +{% block content %} +
    +

    Manage Volumes

    +

    View all volumes. See size, status, and mount point. Destroy volumes.

    +
    + + + + + + + + + + + {% for volume in volumes %} + + + + + + + + + {% endfor %} +
    Volume IDSizeStatusDeviceOperations
    {{volume.id}}{{volume.size}}GB + {% if volume.attach_data.status != None %} + Attached to Instance: {{volume.instance}} + {% else %} + {{volume.status_str}} + {% endif %} + {{volume.attach_data.device}}Destroy
    +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/vpns/index.html b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/vpns/index.html new file mode 100644 index 000000000..5a1e729b0 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/templates/django_nova_syspanel/vpns/index.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% block headerjs %} + + + + + +{% endblock %} + + +{% block breadcrumb %}{{ block.super }}
  • VPNs
  • {% endblock %} + +{% block content %} +
    +

    Manage VPNs

    +

    View all projects VPN status. Schedule launch, termination, and reboot. + Send credentials.

    +
    +
    + {% if status == "success" %} +
    {{message}}
    + {% endif %} + + {% if status == "error" %} +
    {{message}}
    + {% endif %} +
    + + + + + + + + + + + {% for vpn in vpns %} + + + {% if vpn.instance_id %} + + {% else %} + + {% endif %} + + {% if vpn.created_at %} + + {% else %} + + {% endif %} + + {% if vpn.internal_ip %} + + {% else %} + + {% endif %} + + {% if vpn.public_ip %} + + {% else %} + + {% endif %} + + {% ifnotequal vpn.state "pending" %} + {% ifequal vpn.state "running" %} + + {% else %} + + {% endifequal %} + {% else %} + + {% endifnotequal %} + + + + + {% endfor %} + +
    ProjectInstance IdCreatedIPPublicStateOperations
    {{vpn.project_id}}{{vpn.instance_id}}N/A{{vpn.created_at}}N/A{{vpn.internal_ip}}N/A{{vpn.public_ip}}:{{vpn.public_port}}N/A{{vpn.state}}{{vpn.state}}  + {% if vpn.instance_id %} + Terminate | + Restart | + Console Log | + Resend Credentials | + {% else %} + Launch | + Resend Credentials | + {% endif %} +
    +{% endblock %} diff --git a/django-nova-syspanel/src/django_nova_syspanel/urls.py b/django-nova-syspanel/src/django_nova_syspanel/urls.py new file mode 100644 index 000000000..0bd14e178 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/urls.py @@ -0,0 +1,35 @@ +from django.conf.urls.defaults import * +from django.conf import settings +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^$', 'django_nova_syspanel.views.home.index', name='syspanel_index'), + url(r'^instances/$', 'django_nova_syspanel.views.instances.index', name='syspanel_instances'), + url(r'^instances/(?P[^/]+)/terminate$', 'django_nova_syspanel.views.instances.terminate', name='syspanel_instance_terminate'), + url(r'^instances/(?P[^/]+)/restart$', 'django_nova_syspanel.views.instances.restart', name='syspanel_instance_restart'), + url(r'^instances/(?P[^/]+)/console_log$', 'django_nova_syspanel.views.instances.console', name='syspanel_instance_console'), + + url(r'^volumes/$', 'django_nova_syspanel.views.volumes.index', name='syspanel_volumes'), + url(r'^volumes/(?P[^/]+)/delete$', 'django_nova_syspanel.views.volumes.delete', name='syspanel_delete_volume'), + + url(r'^security/$', 'django_nova_syspanel.views.security.index', name='syspanel_security'), + url(r'^security/disable_project_credentials/$', 'django_nova_syspanel.views.security.disable_project_credentials', name='syspanel_security_disable_project_credentials'), + url(r'^security/disable_ip_range/$', 'django_nova_syspanel.views.security.disable_ip', name='syspanel_security_disable_ip_range'), + url(r'^security/disable_public_ips/$', 'django_nova_syspanel.views.security.disable_public_ips', name='syspanel_security_disable_public_ips'), + url(r'^security/disable_vpn/$', 'django_nova_syspanel.views.security.disable_vpn', name='syspanel_security_disable_vpn'), + + url(r'^vpns/$', 'django_nova_syspanel.views.vpns.index', name='syspanel_vpns'), + url(r'^vpns/(?P[^/]+)/launch$', 'django_nova_syspanel.views.vpns.launch', name='syspanel_vpn_launch'), + url(r'^vpns/(?P[^/]+)/send_credentials$', 'django_nova_syspanel.views.vpns.send_credentials', name='syspanel_vpn_send_credentials'), + url(r'^vpns/(?P[^/]+)/terminate$', 'django_nova_syspanel.views.vpns.terminate', name='syspanel_vpn_terminate'), + url(r'^vpns/(?P[^/]+)/restart$', 'django_nova_syspanel.views.vpns.restart', name='syspanel_vpn_restart'), + url(r'^vpns/(?P[^/]+)/console_log$', 'django_nova_syspanel.views.vpns.console', name='syspanel_vpn_console'), + + + url(r'^cloudview/$', 'django_nova_syspanel.views.cloud.index', name='syspanel_cloudview'), + + +) + diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/__init__.py b/django-nova-syspanel/src/django_nova_syspanel/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/cloud.py b/django-nova-syspanel/src/django_nova_syspanel/views/cloud.py new file mode 100644 index 000000000..f2490e932 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/cloud.py @@ -0,0 +1,14 @@ +from django import template +from django.contrib.auth.decorators import login_required +from django.shortcuts import render_to_response + +from django_nova_syspanel.models import get_nova_admin_connection + +@login_required +def index(request): + nova = get_nova_admin_connection() + nodes = nova.get_hosts() + return render_to_response('django_nova_syspanel/cloudview/index.html', { + 'nodes': nodes, + }, context_instance = template.RequestContext(request)) + diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/home.py b/django-nova-syspanel/src/django_nova_syspanel/views/home.py new file mode 100644 index 000000000..141a200e3 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/home.py @@ -0,0 +1,8 @@ +from django import template +from django.contrib.auth.decorators import login_required +from django.shortcuts import render_to_response + +@login_required +def index(request): + return render_to_response('django_nova_syspanel/index.html', { + }, context_instance = template.RequestContext(request)) diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/instances.py b/django-nova-syspanel/src/django_nova_syspanel/views/instances.py new file mode 100644 index 000000000..5452d1c4f --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/instances.py @@ -0,0 +1,81 @@ +from django import template +from django import http +from django.contrib.auth.decorators import login_required +from django.shortcuts import render_to_response +from django_nova_syspanel.models import * + +def _reservations_to_instances(reservation_list): + rv = [] + for r in reservation_list: + for i in r.instances: + i2 = r.__dict__.copy() + i2.update(i.__dict__) + i2['host_name'] = i2['key_name'].split(', ')[1][:-1] + rv.append(i2) + return rv + +@login_required +def index(request): + nova = get_nova_admin_connection() + conn = nova.connection_for("admin", "admin") + reservations = conn.get_all_instances() + instances = _reservations_to_instances(reservations) + return render_to_response('django_nova_syspanel/instances/index.html',{ + 'instances': instances, + }, context_instance = template.RequestContext(request)) + +@login_required +def terminate(request, instance_id): + nova = get_nova_admin_connection() + conn = nova.connection_for("admin", "admin") + reservations = conn.get_all_instances() + instances = _reservations_to_instances(reservations) + try: + conn.terminate_instances([instance_id]) + message = "Instance %s has been scheduled for termination." % \ + instance_id + status = "success" + except: + message = "There were issues trying to terminate instance %s. " \ + "Please try again." % instance_id + status = "error" + # reload instances, maybe get new state + reservations = conn.get_all_instances() + instances = _reservations_to_instances(reservations) + return render_to_response('django_nova_syspanel/instances/index.html',{ + 'instances': instances, + 'message': message, + 'status': status, + }, context_instance = template.RequestContext(request)) + +@login_required +def console(request, instance_id): + nova = get_nova_admin_connection() + conn = nova.connection_for("admin", "admin") + console = conn.get_console_output(instance_id) + response = http.HttpResponse(mimetype='text/plain') + response.write(console.output) + response.flush() + + return response + +@login_required +def restart(request, instance_id): + nova = get_nova_admin_connection() + conn = nova.connection_for("admin", "admin") + try: + conn.reboot_instances([instance_id]) + message = "Instance %s has been scheduled for reboot." % \ + instance_id + status = "success" + except: + message = "There were issues trying to reboot instance %s. " \ + "Please try again." % instance_id + status = "error" + reservations = conn.get_all_instances() + instances = _reservations_to_instances(reservations) + return render_to_response('django_nova_syspanel/instances/index.html',{ + 'instances': instances, + 'message': message, + 'status': status, + }, context_instance = template.RequestContext(request)) diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/security.py b/django-nova-syspanel/src/django_nova_syspanel/views/security.py new file mode 100644 index 000000000..b8c933a8e --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/security.py @@ -0,0 +1,111 @@ +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render_to_response +from django_nova_syspanel.forms import DisableProject +from django_nova_syspanel.forms import DisableIpAddress +from django_nova_syspanel.models import NovaResponseError, get_nova_admin_connection + + +@login_required +def index(request): + disable_project_form = DisableProject() + disable_ip_form = DisableIpAddress() + return render_to_response('django_nova_syspanel/security/index.html',{ + 'project_form':disable_project_form, + 'ip_form':disable_ip_form, + },context_instance = template.RequestContext(request)) + +@login_required +def disable_project_credentials(request): + if request.method == "POST": + nova = get_nova_admin_connection() + form = DisableProject(request.POST) + if form.is_valid(): + name = form.cleaned_data['project_name'] + conn = nova.connection_for(nova.access, name) + vpn = [x for x in nova.get_vpns() if x.project_id == name] + if vpn: + # NOTE(todd): Check, because it could already be shut-off + vpn = vpn[0] + try: + nova.disable_project_credentials(name) + if vpn and vpn.instance_id: + conn.terminate_instances([vpn.instance_id]) + except NovaResponseError, e: + messages.error(request, + 'Unable to disable project %s: %s - %s' % + (name, e.code, e.message)) + return redirect('syspanel_security') + else: + messages.success(request, + 'Project %s has been successfully disabled.' % + form.cleaned_data['project_name']) + return render_to_response( + 'django_nova_syspanel/security/disable_project_credentials.html', + context_instance = template.RequestContext(request)) + else: + messages.error(request, 'Invalid form data') + return redirect('syspanel_security') + else: + return redirect('syspanel_security') + +@login_required +def disable_ip(request): + if request.method == "POST": + conn = get_nova_admin_connection() + form = DisableIpAddress(request.POST) + if form.is_valid(): + try: + conn.block_ips(form.cleaned_data['cidr']) + except NovaResponseError, e: + messages.error(request, + 'Unable to block IPs range %s: %s %s' % + (form.cleaned_data['cidr'], e.code, e.message)) + else: + messages.success(request, + 'IPs range %shas been successfully blocked' % + form.cleaned_data['cidr']) + + return redirect('syspanel_security') + + +@login_required +def disable_public_ips(request): + if request.method == "POST": + try: + nova = get_nova_admin_connection() + nova.disable_all_floating_ips() + except NovaResponseError, e: + messages.error(request, + 'Unable to shut off public IPs: %s - %s' % + (e.code, e.message,)) + else: + messages.success(request, 'Public IPs have been turned off.') + return redirect('syspanel_security') + + +@login_required +def disable_vpn(request): + if request.method == "POST": + nova = get_nova_admin_connection() + conn = nova.connection_for("admin", "admin") + try: + collector = [] + for vpn in nova.get_vpns(): + if not vpn.instance_id: + continue + collector.append(vpn) + if len(collector) >= 5: + conn.terminate_instances([x.instance_id for x in collector]) + collector = [] + if collector: + conn.terminate_instances([x.instance_id for x in collector]) + except NovaResponseError, e: + messages.error(request, 'Unable to shut off all VPNs: %s - %s' % + (e.code, e.message,)) + else: + messages.success(request, 'VPNs have been successfully turned off.') + return redirect('syspanel_security') + else: + return redirect('syspanel_security') diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/volumes.py b/django-nova-syspanel/src/django_nova_syspanel/views/volumes.py new file mode 100644 index 000000000..b3e94903b --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/volumes.py @@ -0,0 +1,55 @@ +from boto.exception import EC2ResponseError +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render_to_response +from django_nova_syspanel.models import * + + +@login_required +def index(request): + nova = get_nova_admin_connection() + conn = nova.connection_for('admin', 'admin') + volumes = conn.get_all_volumes() + + for volume in volumes: + statusstr = str(volume.status)[:-1] + instance = statusstr.split(', ')[-2] + device = statusstr.split(', ')[-1] + status = statusstr.split(' ')[0] + + volume.device = device + volume.instance = instance + volume.status_str = status + + return render_to_response('django_nova_syspanel/volumes/index.html',{ + 'volumes': volumes, + }, context_instance = template.RequestContext(request)) + + +@login_required +def delete(request, volume_id): + nova = get_nova_admin_connection() + conn = nova.connection_for('admin', 'admin') + try: + conn.delete_volume(volume_id) + except EC2ResponseError, e: + messages.error(request, 'Unable to delete volume %s: %s' % \ + (volume_id, e.error_message)) + else: + messages.success(request, + 'Volume %s has been successfully deleted.' % + volume_id) + + volumes = conn.get_all_volumes() + for volume in volumes: + statusstr = str(volume.status)[:-1] + instance = statusstr.split(', ')[-2] + device = statusstr.split(', ')[-1] + status = statusstr.split(' ')[0] + + volume.device = device + volume.instance = instance + volume.status_str = status + return redirect('syspanel_volumes') + diff --git a/django-nova-syspanel/src/django_nova_syspanel/views/vpns.py b/django-nova-syspanel/src/django_nova_syspanel/views/vpns.py new file mode 100644 index 000000000..34c777312 --- /dev/null +++ b/django-nova-syspanel/src/django_nova_syspanel/views/vpns.py @@ -0,0 +1,59 @@ +from django import http +from django import template +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render_to_response +from django_nova_syspanel.models import get_nova_admin_connection + + +@login_required +def index(request): + nova = get_nova_admin_connection() + vpns = nova.get_vpns() + return render_to_response('django_nova_syspanel/vpns/index.html',{ + 'vpns': vpns, + }, context_instance = template.RequestContext(request)) + + +@login_required +def console(request, project_id): + nova = get_nova_admin_connection() + conn = nova.connection_for(nova.access, project_id) + vpn = [x for x in nova.get_vpns() if x.project_id == project_id][0] + console = conn.get_console_output(vpn.instance_id) + response = http.HttpResponse(mimetype='text/plain') + response.write(console.output) + response.flush() + return response + + +@login_required +def restart(request, project_id): + nova = get_nova_admin_connection() + conn = nova.connection_for(nova.access, project_id) + vpn = [x for x in nova.get_vpns() if x.project_id == project_id][0] + conn.reboot_instances([vpn.instance_id]) + return redirect('django_nova_syspanel/vpns') + + +@login_required +def terminate(request, project_id): + nova = get_nova_admin_connection() + conn = nova.connection_for(nova.access, project_id) + vpn = [x for x in nova.get_vpns() if x.project_id == project_id][0] + conn.terminate_instances([vpn.instance_id]) + return redirect('django_nova_syspanel/vpns') + + +@login_required +def launch(request, project_id): + nova = get_nova_admin_connection() + nova.start_vpn(project_id) + return redirect('django_nova_syspanel/vpns') + + +@login_required +def send_credentials(request, project_id, user_id): + # TODO we need to pass a user id as well + nova = get_nova_admin_connection() + nova.get_zip(user_id, project_id) + return redirect('django_nova_syspanel/vpns') diff --git a/openstack-dashboard/dashboard/settings.py b/openstack-dashboard/dashboard/settings.py index 2a3742127..62c62acc2 100644 --- a/openstack-dashboard/dashboard/settings.py +++ b/openstack-dashboard/dashboard/settings.py @@ -65,6 +65,7 @@ INSTALLED_APPS = ( 'django.contrib.syndication', 'django_nose', 'django_nova', + 'django_nova_syspanel', 'registration', ) diff --git a/openstack-dashboard/dashboard/urls.py b/openstack-dashboard/dashboard/urls.py index 485fe031b..f5cd11350 100644 --- a/openstack-dashboard/dashboard/urls.py +++ b/openstack-dashboard/dashboard/urls.py @@ -40,6 +40,7 @@ urlpatterns = patterns('', url(r'^admin/project/', include('django_nova.urls.admin_project')), url(r'^admin/roles/', include('django_nova.urls.admin_roles')), url(r'^admin/', include(admin.site.urls)), + url(r'^syspanel/', include('django_nova_syspanel.urls')), ) urlpatterns += patterns('django.views.generic.simple', diff --git a/openstack-dashboard/tools/install_venv.py b/openstack-dashboard/tools/install_venv.py index 930ddb89b..c573282a1 100644 --- a/openstack-dashboard/tools/install_venv.py +++ b/openstack-dashboard/tools/install_venv.py @@ -105,6 +105,12 @@ def install_django_nova(): run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=path) +def install_django_nova_syspanel(): + print 'Installing django_nova_syspanel in development mode...' + path = os.path.join(ROOT, '..', 'django-nova-syspanel') + run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=path) + + def print_summary(): summary = """ OpenStack Dashboard development environment setup is complete. @@ -122,6 +128,7 @@ def main(): create_virtualenv() install_dependencies() install_django_nova() + install_django_nova_syspanel() print_summary() if __name__ == '__main__':