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 }}
View servers that participate in your cloud. See statistics about + running services and served objects.
+Hostname | +Compute Status | +Instance Count | +Volume Status | +Volume Count | +||||
---|---|---|---|---|---|---|---|---|
{{node.hostname}} | + {% if node.compute %} +{{node.compute}} | +{{node.instance_count}} | + {% else %} +N/A | +N/A | + {% endif %} + + {% if node.volume %} +{{node.volume}} | +{{node.volume_count}} | + {% else %} +N/A | +N/A | + {% endif %} + +
View all running instances other than VPNs. Schedule instances for + reboot or termination. Download logs.
+ Manage Instances → +View all volumes. See size, status, and mount point. Destroy volumes.
+ Manage Volumes → +View all projects VPN status. Schedule launch, termination, and reboot. + Send credentials.
+ Manage VPN → +View servers that participate in your cloud. See statistics about + running services and served objects.
+ Cloud View → +Perform operations to deal with rogue users, limit or prevent compromise, + and deal with external threats.
+ Security → +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 → #} +{#View all running instances other than VPNs. Schedule instances for + reboot or termination. Download logs.
+Id | +User | +Node | +Created | +Image | +IP | +Public IP | +Volumes | +State | +Operations | +
---|---|---|---|---|---|---|---|---|---|
{{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 + | +
+ 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 }}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.
+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.
+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.
+Scenario: Issue related to VPN service affecting multiple customers.
+Action: Disable VPN service. This should turn off the VPN ip.
+View all volumes. See size, status, and mount point. Destroy volumes.
+Volume ID | +Size | +Status | +Device | +Operations | +
---|---|---|---|---|
{{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 | +
View all projects VPN status. Schedule launch, termination, and reboot. + Send credentials.
+Project | +Instance Id | +Created | +IP | +Public | +State | +Operations | +||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
{{vpn.project_id}} | + {% if vpn.instance_id %} +{{vpn.instance_id}} | + {% else %} +N/A | + {% endif %} + + {% if vpn.created_at %} +{{vpn.created_at}} | + {% else %} +N/A | + {% endif %} + + {% if vpn.internal_ip %} +{{vpn.internal_ip}} | + {% else %} +N/A | + {% endif %} + + {% if vpn.public_ip %} +{{vpn.public_ip}}:{{vpn.public_port}} | + {% else %} +N/A | + {% endif %} + + {% ifnotequal vpn.state "pending" %} + {% ifequal vpn.state "running" %} +{{vpn.state}} | + {% else %} +{{vpn.state}} | + {% endifequal %} + {% else %} ++ {% endifnotequal %} + + | + {% if vpn.instance_id %} + Terminate | + Restart | + Console Log | + Resend Credentials | + {% else %} + Launch | + Resend Credentials | + {% endif %} + | +