diff options
188 files changed, 6872 insertions, 72 deletions
@@ -1,3 +1,4 @@ -.dashboard-venv -local/dashboard_openstack.sqlite3 -local/local_settings.py +django-nova/src/django_nova.egg-info +openstack-dashboard/.dashboard-venv +openstack-dashboard/local/dashboard_openstack.sqlite3 +openstack-dashboard/local/local_settings.py @@ -1,49 +0,0 @@ -OpenStack Dashboard -------------------- - -The OpenStack Dashboard is a reference implementation of a Django site that -uses the Django-Nova project to provide web based interactions with the -OpenStack Nova cloud controller. - -For more information about the Django-Nova project, please visit: - - http://launchpad.net/django-nova - - -Getting Started ---------------- - -The first step is to obtain a local copy of the django-nova project: - - $ mkdir django-nova - $ cd django-nova - $ bzr init-repo . - $ bzr branch lp:django-nova/trunk - - -Next we will create the virtualenv for local development. A tool is included to -create one for you: - - $ python tools/install_venv.py <path to django-nova/trunk> - - -Now that the virtualenv is created, you need to configure your local -environment. To do this, create a local_settings.py file in the local/ -directory. There is a local_settings.py.example file there that may be used -as a template. - -Finally, issue the django syncdb command: - - $ tools/with_venv.sh dashboard/manage.py syncdb - -If after you have specified the admin user the script appears to hang, it -probably means the installation of Nova being referred to in local_settings.py -is unavailable. - - -If all is well you should now able to run the server locally: - - $ tools/with_venv.sh dashboard/manage.py runserver - - - diff --git a/django-nova/LICENSE b/django-nova/LICENSE new file mode 100644 index 00000000..68c771a0 --- /dev/null +++ b/django-nova/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/README b/django-nova/README new file mode 100644 index 00000000..cc89b6cf --- /dev/null +++ b/django-nova/README @@ -0,0 +1,42 @@ +OpenStack Django-Nova +--------------------- + +The Django-Nova project is a Django module that is used to provide web based +interactions with the OpenStack Nova cloud controller. + +There is a reference implementation that uses this module located at: + + http://launchpad.net/openstack-dashboard + +It is highly recommended that you make use of this reference implementation +so that changes you make can be visualized effectively and are consistent. +Using this reference implementation as a development environment will greatly +simplify development of the django-nova module. + +Of course, if you are developing your own Django site using django-nova, then +you can disregard this advice. + + + +Getting Started +--------------- + +Django-Nova 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 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/bootstrap.py b/django-nova/bootstrap.py new file mode 100644 index 00000000..5f2cb083 --- /dev/null +++ b/django-nova/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/buildout.cfg b/django-nova/buildout.cfg new file mode 100644 index 00000000..c41ec9ae --- /dev/null +++ b/django-nova/buildout.cfg @@ -0,0 +1,19 @@ +[buildout] +parts = python django +develop = . +eggs = django-nova + +[python] +recipe = zc.recipe.egg +interpreter = python +eggs = ${buildout:eggs} + +[django] +recipe = djangorecipe +version = 1.2.4 +project = django_nova +projectegg = django_nova +settings = testsettings +test = django_nova +eggs = ${buildout:eggs} + diff --git a/django-nova/setup.py b/django-nova/setup.py new file mode 100644 index 00000000..838e652f --- /dev/null +++ b/django-nova/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", + version = "0.1", + url = 'https://launchpad.net/django-nova/', + license = 'Apache 2.0', + description = "A Django interface for OpenStack Nova.", + long_description = read('README'), + author = 'Devin Carlen', + author_email = 'devin.carlen@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/dashboard/__init__.py b/django-nova/src/django_nova/__init__.py index e69de29b..e69de29b 100644 --- a/dashboard/__init__.py +++ b/django-nova/src/django_nova/__init__.py diff --git a/django-nova/src/django_nova/adminclient.py b/django-nova/src/django_nova/adminclient.py new file mode 100644 index 00000000..a6af95ca --- /dev/null +++ b/django-nova/src/django_nova/adminclient.py @@ -0,0 +1,498 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Nova User API client library. +""" + +import base64 +import boto +import boto.exception +import httplib +import re +import string +from boto.ec2.regioninfo import RegionInfo + + +DEFAULT_CLC_URL='http://127.0.0.1:8773' +DEFAULT_REGION='nova' +DEFAULT_ACCESS_KEY='admin' +DEFAULT_SECRET_KEY='admin' + + +class UserInfo(object): + """ + Information about a Nova user, as parsed through SAX + fields include: + username + accesskey + secretkey + + and an optional field containing a zip with X509 cert & rc + file + """ + + def __init__(self, connection=None, username=None, endpoint=None): + self.connection = connection + self.username = username + self.endpoint = endpoint + + def __repr__(self): + return 'UserInfo:%s' % self.username + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'username': + self.username = str(value) + elif name == 'file': + self.file = base64.b64decode(str(value)) + elif name == 'accesskey': + self.accesskey = str(value) + elif name == 'secretkey': + self.secretkey = str(value) + + +class UserRole(object): + """ + Information about a Nova user's role, as parsed through SAX. + Fields include: + role + """ + def __init__(self, connection=None): + self.connection = connection + self.role = None + + def __repr__(self): + return 'UserRole:%s' % self.role + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'role': + self.role = value + else: + setattr(self, name, str(value)) + + +class ProjectInfo(object): + """ + Information about a Nova project, as parsed through SAX + Fields include: + projectname + description + projectManagerId + memberIds + """ + + def __init__(self, connection=None): + self.connection = connection + self.projectname = None + self.description = None + self.projectManagerId = None + self.memberIds = [] + + def __repr__(self): + return 'ProjectInfo:%s' % self.projectname + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'projectname': + self.projectname = value + elif name == 'description': + self.description = value + elif name == 'projectManagerId': + self.projectManagerId = value + elif name == 'memberId': + self.memberIds.append(value) + else: + setattr(self, name, str(value)) + + +class ProjectMember(object): + """ + Information about a Nova project member, as parsed through SAX. + Fields include: + memberId + """ + + def __init__(self, connection=None): + self.connection = connection + self.memberId = None + + def __repr__(self): + return 'ProjectMember:%s' % self.memberId + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'member': + self.memberId = value + else: + setattr(self, name, str(value)) + + +class HostInfo(object): + """ + Information about a Nova Host, as parsed through SAX: + Hostname + Compute Service Status + Volume Service Status + """ + + def __init__(self, connection=None): + self.connection = connection + self.hostname = None + self.compute = None + self.volume = None + self.instance_count = 0 + self.volume_count = 0 + + def __repr__(self): + return 'Host:%s' % self.hostname + + # this is needed by the sax parser, so ignore the ugly name + def startElement(self, name, attrs, connection): + return None + + # this is needed by the sax parser, so ignore the ugly name + def endElement(self, name, value, connection): + fixed_name = string.lower(re.sub(r'([A-Z])', r'_\1', name)) + setattr(self, fixed_name, value) + + +class Vpn(object): + """ + Information about a Vpn, as parsed through SAX + fields include: + instance_id + project_id + public_ip + public_port + created_at + internal_ip + state + """ + + def __init__(self, connection=None): + self.connection = connection + self.instance_id = None + self.project_id = None + + def __repr__(self): + return 'Vpn:%s:%s' % (self.project_id, self.instance_id) + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'instanceId': + self.instance_id = str(value) + elif name == 'projectId': + self.project_id = str(value) + elif name == 'publicIp': + self.public_ip = str(value) + elif name == 'publicPort': + self.public_port = str(value) + elif name == 'createdAt': + self.created_at = str(value) + elif name == 'internalIp': + self.internal_ip = str(value) + else: + setattr(self, name, str(value)) + + +class InstanceType(object): + """ + Information about a Nova instance type, as parsed through SAX. + + **Fields include** + + * name + * vcpus + * disk_gb + * memory_mb + * flavor_id + + """ + + def __init__(self, connection=None): + self.connection = connection + self.name = None + self.vcpus = None + self.disk_gb = None + self.memory_mb = None + self.flavor_id = None + + def __repr__(self): + return 'InstanceType:%s' % self.name + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == "memoryMb": + self.memory_mb = str(value) + elif name == "flavorId": + self.flavor_id = str(value) + elif name == "diskGb": + self.disk_gb = str(value) + else: + setattr(self, name, str(value)) + + +class StatusResponse(object): + def __init__(self, connection=None): + self.connection = connection + self.status = None + + def __repr__(self): + return 'Status:%s' % self.status + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + setattr(self, name, str(value)) + + +class NovaAdminClient(object): + def __init__(self, clc_url=DEFAULT_CLC_URL, region=DEFAULT_REGION, + access_key=DEFAULT_ACCESS_KEY, secret_key=DEFAULT_SECRET_KEY, + **kwargs): + parts = self.split_clc_url(clc_url) + + self.clc_url = clc_url + self.region = region + self.access = access_key + self.secret = secret_key + self.apiconn = boto.connect_ec2(aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + is_secure=parts['is_secure'], + region=RegionInfo(None, + region, + parts['ip']), + port=parts['port'], + path='/services/Admin', + **kwargs) + self.apiconn.APIVersion = 'nova' + + def connection_for(self, username, project, clc_url=None, region=None, + **kwargs): + """ + Returns a boto ec2 connection for the given username. + """ + if not clc_url: + clc_url = self.clc_url + if not region: + region = self.region + parts = self.split_clc_url(clc_url) + user = self.get_user(username) + access_key = '%s:%s' % (user.accesskey, project) + return boto.connect_ec2(aws_access_key_id=access_key, + aws_secret_access_key=user.secretkey, + is_secure=parts['is_secure'], + region=RegionInfo(None, + self.region, + parts['ip']), + port=parts['port'], + path='/services/Cloud', + **kwargs) + + def split_clc_url(self, clc_url): + """ + Splits a cloud controller endpoint url. + """ + parts = httplib.urlsplit(clc_url) + is_secure = parts.scheme == 'https' + ip, port = parts.netloc.split(':') + return {'ip': ip, 'port': int(port), 'is_secure': is_secure} + + def get_users(self): + """ grabs the list of all users """ + return self.apiconn.get_list('DescribeUsers', {}, [('item', UserInfo)]) + + def get_user(self, name): + """ grab a single user by name """ + try: + return self.apiconn.get_object('DescribeUser', {'Name': name}, UserInfo) + except boto.exception.BotoServerError, e: + if e.status == 400 and e.error_code == 'NotFound': + return None + raise + + def has_user(self, username): + """ determine if user exists """ + return self.get_user(username) != None + + def create_user(self, username): + """ creates a new user, returning the userinfo object with access/secret """ + return self.apiconn.get_object('RegisterUser', {'Name': username}, UserInfo) + + def delete_user(self, username): + """ deletes a user """ + return self.apiconn.get_object('DeregisterUser', {'Name': username}, UserInfo) + + def get_roles(self, project_roles=True): + """Returns a list of available roles.""" + return self.apiconn.get_list('DescribeRoles', + {'ProjectRoles': project_roles}, + [('item', UserRole)]) + + def get_user_roles(self, user, project=None): + """Returns a list of roles for the given user. + Omitting project will return any global roles that the user has. + Specifying project will return only project specific roles. + """ + params = {'User':user} + if project: + params['Project'] = project + return self.apiconn.get_list('DescribeUserRoles', + params, + [('item', UserRole)]) + + def add_user_role(self, user, role, project=None): + """ + Add a role to a user either globally or for a specific project. + """ + return self.modify_user_role(user, role, project=project, + operation='add') + + def remove_user_role(self, user, role, project=None): + """ + Remove a role from a user either globally or for a specific project. + """ + return self.modify_user_role(user, role, project=project, + operation='remove') + + def modify_user_role(self, user, role, project=None, operation='add', + **kwargs): + """ + Add or remove a role for a user and project. + """ + params = {'User': user, + 'Role': role, + 'Project': project, + 'Operation': operation} + return self.apiconn.get_status('ModifyUserRole', params) + + def get_projects(self, user=None): + """ + Returns a list of all projects. + """ + if user: + params = {'User': user} + else: + params = {} + return self.apiconn.get_list('DescribeProjects', + params, + [('item', ProjectInfo)]) + + def get_project(self, name): + """ + Returns a single project with the specified name. + """ + project = self.apiconn.get_object('DescribeProject', + {'Name': name}, + ProjectInfo) + + if project.projectname != None: + return project + + def create_project(self, projectname, manager_user, description=None, + member_users=None): + """ + Creates a new project. + """ + params = {'Name': projectname, + 'ManagerUser': manager_user, + 'Description': description, + 'MemberUsers': member_users} + return self.apiconn.get_object('RegisterProject', params, ProjectInfo) + + def delete_project(self, projectname): + """ + Permanently deletes the specified project. + """ + return self.apiconn.get_object('DeregisterProject', + {'Name': projectname}, + ProjectInfo) + + def get_project_members(self, name): + """ + Returns a list of members of a project. + """ + return self.apiconn.get_list('DescribeProjectMembers', + {'Name': name}, + [('item', ProjectMember)]) + + def add_project_member(self, user, project): + """ + Adds a user to a project. + """ + return self.modify_project_member(user, project, operation='add') + + def remove_project_member(self, user, project): + """ + Removes a user from a project. + """ + return self.modify_project_member(user, project, operation='remove') + + def modify_project_member(self, user, project, operation='add'): + """ + Adds or removes a user from a project. + """ + params = {'User': user, + 'Project': project, + 'Operation': operation} + return self.apiconn.get_status('ModifyProjectMember', params) + + def get_zip(self, user, project): + """ + Returns the content of a zip file containing novarc and access credentials. + """ + params = {'Name': user, 'Project': project} + zip = self.apiconn.get_object('GenerateX509ForUser', params, UserInfo) + return zip.file + + def start_vpn(self, project): + """ + Starts the vpn for a user + """ + return self.apiconn.get_object('StartVpn', {'Project': project}, Vpn) + + def get_vpns(self): + """Return a list of vpn with project name""" + return self.apiconn.get_list('DescribeVpns', {}, [('item', Vpn)]) + + def get_hosts(self): + return self.apiconn.get_list('DescribeHosts', {}, [('item', HostInfo)]) + + def get_instance_types(self): + """Grabs the list of all users.""" + return self.apiconn.get_list('DescribeInstanceTypes', {}, + [('item', InstanceType)]) + + def disable_project_credentials(self, project): + """Revoke project credentials and kill the cloudpipe/vpn instance""" + return self.apiconn.get_object('DisableProjectCredentials', + {'Project': project}, StatusResponse) diff --git a/django-nova/src/django_nova/connection.py b/django-nova/src/django_nova/connection.py new file mode 100644 index 00000000..b29146ed --- /dev/null +++ b/django-nova/src/django_nova/connection.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Manage connections to Nova's admin API. +""" + +from django.conf import settings +from django_nova import adminclient + + +def get_nova_admin_connection(): + """ + Returns a Nova administration connection. + """ + return adminclient.NovaAdminClient ( + clc_url=settings.NOVA_DEFAULT_ENDPOINT, + region=settings.NOVA_DEFAULT_REGION, + access_key=settings.NOVA_ACCESS_KEY, + secret_key=settings.NOVA_SECRET_KEY + ) + + + diff --git a/django-nova/src/django_nova/exceptions.py b/django-nova/src/django_nova/exceptions.py new file mode 100644 index 00000000..03f3a986 --- /dev/null +++ b/django-nova/src/django_nova/exceptions.py @@ -0,0 +1,95 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Better wrappers for errors from Nova's admin api. +""" + +import boto.exception +from django.shortcuts import redirect +from django.core import exceptions as core_exceptions + + +class NovaServerError(Exception): + """ + Consumes a BotoServerError and gives more meaningful errors. + """ + def __init__(self, ec2error): + self.status = ec2error.status + self.message = ec2error.reason + + def __str__(self): + return self.message + + +class NovaApiError(Exception): + """ + Used when Nova returns a 400 Bad Request status. + """ + def __init__(self, ec2error): + self.message = ec2error.error_message + + def __str__(self): + return self.message + + +class NovaUnavailableError(NovaServerError): + """ + Used when Nova returns a 503 Service Unavailable status. + """ + pass + + +class NovaUnauthorizedError(core_exceptions.PermissionDenied): + """ + Used when Nova returns a 401 Not Authorized status. + """ + pass + + +def wrap_nova_error(func): + """ + Used to decorate a function that interacts with boto. It will catch + and convert boto server errors and reraise as a more specific nova error. + """ + def decorator(*args, **kwargs): + try: + return func(*args, **kwargs) + except boto.exception.BotoServerError, e: + if e.status == 400 and e.error_code == 'ApiError': + raise NovaApiError(e) + elif e.status == 401: + raise NovaUnauthorizedError() + elif e.status == 503: + raise NovaUnavailableError(e) + raise NovaServerError(e) + return decorator + + +def handle_nova_error(func): + """ + Decorator for handling nova errors in a generalized way. + """ + def decorator(*args, **kwargs): + try: + return func(*args, **kwargs) + except NovaUnavailableError: + return redirect('nova_unavailable') + return decorator + + diff --git a/django-nova/src/django_nova/forms.py b/django-nova/src/django_nova/forms.py new file mode 100644 index 00000000..cc8ae265 --- /dev/null +++ b/django-nova/src/django_nova/forms.py @@ -0,0 +1,262 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Forms used by various views. +""" + +import re + +from django import forms +from django.contrib.auth import models as auth_models +from django_nova.connection import get_nova_admin_connection +from django_nova.exceptions import wrap_nova_error + + +# TODO: Store this in settings. +MAX_VOLUME_SIZE = 100 + +alphanumeric_re = re.compile(r'^\w+$') + + +@wrap_nova_error +def get_instance_type_choices(): + """ + Returns list of instance types from nova admin api + """ + nova = get_nova_admin_connection() + instance_types = nova.get_instance_types() + rv = [] + for t in instance_types: + rv.append((t.name, "%s (%sMB memory, %s cpu, %sGB space)" % \ + (t.name, t.memory_mb, t.vcpus, t.disk_gb))) + return rv + +def get_instance_choices(project): + choices = [(i.id, i.id) for i in project.get_instances()] + if not len(choices): + choices = [('', 'none available')] + return choices + +def get_key_pair_choices(project): + choices = [(k.name, k.name) for k in project.get_key_pairs()] + if not len(choices): + choices = [('', 'none available')] + return choices + +#def get_security_group_choices(project): +# choices = [(g.name, g.description) for g in project.get_security_groups()] +# if len(choices) == 0: +# choices = [('', 'none available')] +# return choices + +def get_available_volume_choices(project): + choices = [(v.id, '%s %s - %dGB' % (v.id, v.displayName, v.size)) for v in \ + project.get_volumes() if v.status != "in-use"] + if not len(choices): + choices = [('', 'none available')] + return choices + +def get_protocols(): + return ( + ('tcp', 'tcp'), + ('udp', 'udp'), + ) + +@wrap_nova_error +def get_roles(project_roles=True): + nova = get_nova_admin_connection() + roles = nova.get_roles(project_roles=project_roles) + return [(role.role, role.role) for role in roles] + +@wrap_nova_error +def get_members(project): + nova = get_nova_admin_connection() + members = nova.get_project_members(project) + return [str(user.memberId) for user in members] + +@wrap_nova_error +def set_project_roles(projectname, username, roles): + nova = get_nova_admin_connection() + # hacky work around to interface correctly with multiple select form + _remove_roles(projectname, username) + + for role in roles: + nova.add_user_role(username, str(role), projectname) + +def _remove_roles(project, username): + nova = get_nova_admin_connection() + userroles = nova.get_user_roles(username, project) + roles = [str(role.role) for role in userroles] + + for role in roles: + if role == "developer": + nova.remove_user_role(username, "developer", project) + if role == "sysadmin": + nova.remove_user_role(username, "sysadmin", project) + if role == "netadmin": + nova.remove_user_role(username, "netadmin", project) + + +class ProjectFormBase(forms.Form): + def __init__(self, project, *args, **kwargs): + self.project = project + super(ProjectFormBase, self).__init__(*args, **kwargs) + + +class LaunchInstanceForm(forms.Form): + # nickname = forms.CharField() + # description = forms.CharField() + + count = forms.ChoiceField(choices=[(x, x) for x in range(1, 6)]) + size = forms.ChoiceField() + key_name = forms.ChoiceField() + #security_group = forms.ChoiceField() + user_data = forms.CharField(required=False, widget=forms.widgets.Textarea(attrs={'rows': 4})) + + def __init__(self, project, *args, **kwargs): + forms.Form.__init__(self, *args, **kwargs) + #self.fields['security_group'].choices = get_security_group_choices(project) + self.fields['key_name'].choices = get_key_pair_choices(project) + self.fields['size'].choices = get_instance_type_choices() + + +class UpdateInstanceForm(forms.Form): + nickname = forms.CharField(required=False, label="Name") + description = forms.CharField(required=False, widget=forms.Textarea, max_length=70) + + def __init__(self, instance, *args, **kwargs): + forms.Form.__init__(self, *args, **kwargs) + self.fields['nickname'].initial = instance.displayName + self.fields['description'].initial = instance.displayDescription + + +class UpdateImageForm(forms.Form): + nickname = forms.CharField(required=False, label="Name") + description = forms.CharField(required=False, widget=forms.Textarea, max_length=70) + + def __init__(self, image, *args, **kwargs): + forms.Form.__init__(self, *args, **kwargs) + self.fields['nickname'].initial = image.displayName + self.fields['description'].initial = image.description + + +class CreateKeyPairForm(ProjectFormBase): + name = forms.RegexField(regex=alphanumeric_re) + + def clean_name(self): + name = self.cleaned_data['name'] + + if self.project.has_key_pair(name): + raise forms.ValidationError('A key named %s already exists.' % name) + + return name + + +class CreateSecurityGroupForm(ProjectFormBase): + name = forms.RegexField(regex=alphanumeric_re) + description = forms.CharField() + + def clean_name(self): + name = self.cleaned_data['name'] + + if self.project.has_security_group(name): + raise forms.ValidationError('A security group named %s already exists.' % name) + + return name + + +class AuthorizeSecurityGroupRuleForm(forms.Form): + protocol = forms.ChoiceField(choices=get_protocols()) + from_port = forms.IntegerField(min_value=1, max_value=65535) + to_port = forms.IntegerField(min_value=1, max_value=65535) + + +class CreateVolumeForm(forms.Form): + size = forms.IntegerField(label='Size (in GB)', min_value=1, max_value=MAX_VOLUME_SIZE) + nickname = forms.CharField() + description = forms.CharField() + + +class AttachVolumeForm(ProjectFormBase): + volume = forms.ChoiceField() + instance = forms.ChoiceField() + device = forms.CharField(initial='/dev/vdb') + + def __init__(self, project, *args, **kwargs): + super(AttachVolumeForm, self).__init__(project, *args, **kwargs) + self.fields['volume'].choices = get_available_volume_choices(project) + self.fields['instance'].choices = get_instance_choices(project) + + +class ProjectForm(forms.Form): + projectname = forms.CharField(label="Project Name", max_length=20) + description = forms.CharField(label="Description", + widget=forms.widgets.Textarea()) + manager = forms.ModelChoiceField(queryset=auth_models.User.objects.all(), + label="Project Manager") + + +class GlobalRolesForm(forms.Form): + role = forms.MultipleChoiceField(label='Roles', required=False) + + def __init__(self, *args, **kwargs): + super(GlobalRolesForm, self).__init__(*args, **kwargs) + self.fields['role'].choices = get_roles(project_roles=False) + + +class ProjectUserForm(forms.Form): + role = forms.MultipleChoiceField(label='Roles', required=False) + + def __init__(self, project, user, *args, **kwargs): + super(ProjectUserForm, self).__init__(*args, **kwargs) + self.project = project + self.user = user + self.fields['role'].choices = get_roles() + + def save(self): + set_project_roles(self.project.projectname, + self.user.username, + self.cleaned_data['role']) + + +class AddProjectUserForm(forms.Form): + username = forms.ModelChoiceField(queryset='', + label='Username', + empty_label='Select a Username') + role = forms.MultipleChoiceField(label='Roles') + + def __init__(self, *args, **kwargs): + project = kwargs.pop('project') + super(AddProjectUserForm, self).__init__(*args, **kwargs) + members = get_members(project) + + self.fields['username'].queryset = \ + auth_models.User.objects.exclude(username__in=members) + self.fields['role'].choices = get_roles() + + +class SendCredentialsForm(forms.Form): + users = forms.MultipleChoiceField(label='Users', required=True) + + def __init__(self, *args, **kwargs): + query_list = kwargs.pop('query_list') + super(SendCredentialsForm, self).__init__(*args, **kwargs) + + self.fields['users'].choices = [(choices, choices) for choices in query_list] + diff --git a/local/__init__.py b/django-nova/src/django_nova/management/__init__.py index e69de29b..e69de29b 100644 --- a/local/__init__.py +++ b/django-nova/src/django_nova/management/__init__.py diff --git a/django-nova/src/django_nova/management/commands/__init__.py b/django-nova/src/django_nova/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/django-nova/src/django_nova/management/commands/__init__.py diff --git a/django-nova/src/django_nova/management/commands/createnovausers.py b/django-nova/src/django_nova/management/commands/createnovausers.py new file mode 100644 index 00000000..18bb461d --- /dev/null +++ b/django-nova/src/django_nova/management/commands/createnovausers.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Management commands for synchronizing the Django auth database and Nova +users database. +""" + +from django.core.management.base import NoArgsCommand +from django.contrib.auth.models import User +from django_nova.connection import get_nova_admin_connection + +class Command(NoArgsCommand): + help = 'Creates nova users for all users in the django auth database.' + + def handle_noargs(self, **options): + nova = get_nova_admin_connection() + users = User.objects.all() + for user in users: + if not nova.has_user(user.username): + self.stdout.write('creating user %s... ' % user.username) + nova.create_user(user.username) + self.stdout.write('ok\n') diff --git a/django-nova/src/django_nova/manager.py b/django-nova/src/django_nova/manager.py new file mode 100644 index 00000000..4cdb892a --- /dev/null +++ b/django-nova/src/django_nova/manager.py @@ -0,0 +1,340 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Simple API for interacting with Nova projects. +""" + +import boto +import boto.ec2.volume +import boto.exception +import boto.s3 +from django.conf import settings +from django_nova.connection import get_nova_admin_connection +from django_nova.exceptions import wrap_nova_error + + +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 + + @wrap_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) + + @wrap_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) + + @wrap_nova_error + def modify_image_attribute(self, image_id, attribute=None, operation=None, + groups='all'): + conn = self.get_nova_connection() + return conn.modify_image_attribute(image_id, + attribute='launchPermission', + operation='remove', + groups='all',) + + + @wrap_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 + + @wrap_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 + + @wrap_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 + + @wrap_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 boto.exception.S3ResponseError, e: + if e.code == "NoSuchBucket": + return None + else: + raise e + + key = bucket.get_key(graph_name) + + return key.read() + + @wrap_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]) + + @wrap_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 + + @wrap_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 + + @wrap_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) is not None + + @wrap_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) + + @wrap_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) + + @wrap_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' + ) + + @wrap_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' + ) + + @wrap_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 + + @wrap_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 + + @wrap_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 + + @wrap_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) + + @wrap_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) + + @wrap_nova_error + def get_volumes(self): + """ + Returns all volumes in this project. + """ + conn = self.get_nova_connection() + return conn.get_all_volumes() + + @wrap_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) + + @wrap_nova_error + def delete_volume(self, volume_id): + conn = self.get_nova_connection() + return conn.delete_volume(volume_id) + + @wrap_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) + + @wrap_nova_error + def detach_volume(self, volume_id): + conn = self.get_nova_connection() + return conn.detach_volume(volume_id) + diff --git a/django-nova/src/django_nova/models.py b/django-nova/src/django_nova/models.py new file mode 100644 index 00000000..5f06f251 --- /dev/null +++ b/django-nova/src/django_nova/models.py @@ -0,0 +1,121 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Database models for authorization credentials and synchronizing Nova users. +""" + +import datetime +import random +import re +import sha +from django.conf import settings +from django.contrib.auth import models as auth_models +from django.contrib.sites import models as site_models +from django.core import mail +from django.db import models +from django.db.models.signals import post_save +from django.template.loader import render_to_string +from django_nova.connection import get_nova_admin_connection + + +SHA1_RE=re.compile('^[a-f0-9]{40}$') + + +class CredentialsAuthorization(models.Model): + username = models.CharField(max_length=128) + project = models.CharField(max_length=128) + auth_token = models.CharField(max_length=40) + auth_date = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return '%s/%s:%s' % (self.username, self.project, self.auth_token) + + @classmethod + def get_by_token(cls, token): + if SHA1_RE.search(token): + try: + credentials = cls.objects.get(auth_token=token) + except cls.DoesNotExist: + return None + if not credentials.auth_token_expired(): + return credentials + return None + + @classmethod + def authorize(cls, username, project): + return cls.objects.create(username=username, + project=project, + auth_token=cls.create_auth_token(username)) + + @staticmethod + def create_auth_token(username): + salt = sha.new(str(random.random())).hexdigest()[:5] + return sha.new(salt+username).hexdigest() + + def auth_token_expired(self): + expiration_date = datetime.timedelta(days=int(settings.CREDENTIAL_AUTHORIZATION_DAYS)) + + return self.auth_date + expiration_date <= datetime.datetime.now() + + def get_download_url(self): + return settings.CREDENTIAL_DOWNLOAD_URL + self.auth_token + + def get_zip(self): + nova = get_nova_admin_connection() + self.delete() + return nova.get_zip(self.username, self.project) + + +def credentials_post_save(sender, instance, created, *args, **kwargs): + """ + Creates a Nova User when a new Django User is created. + """ + if created: + site = site_models.Site.objects.get_current() + user = auth_models.User.objects.get(username=instance.username) + context = { + 'user': user, + 'download_url': instance.get_download_url(), + 'dashboard_url': 'http://%s/' % site.domain + } + subject = render_to_string('credentials/credentials_email_subject.txt') + body = render_to_string('credentials/credentials_email.txt', context) + + message = mail.EmailMessage(subject=subject, body=body, to=[user.email]) + message.send(fail_silently=False) +post_save.connect(credentials_post_save, + CredentialsAuthorization, + dispatch_uid='django_nova.CredentialsAuthorization.post_save') + + +def user_post_save(sender, instance, created, *args, **kwargs): + """ + Creates a Nova User when a new Django User is created. + """ + + # NOTE(devcamcar): If running unit tests, don't use a real endpoint. + if settings.NOVA_DEFAULT_ENDPOINT == 'none': + return + + 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, + auth_models.User, + dispatch_uid='django_nova.User.post_save') diff --git a/django-nova/src/django_nova/shortcuts.py b/django-nova/src/django_nova/shortcuts.py new file mode 100644 index 00000000..15474d0b --- /dev/null +++ b/django-nova/src/django_nova/shortcuts.py @@ -0,0 +1,131 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Helper methods for commonly used operations. +""" + +from django.conf import settings +from django.core.cache import cache +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django_nova import manager +from django_nova.connection import get_nova_admin_connection +from django_nova.exceptions import wrap_nova_error + + +@wrap_nova_error +def get_project_or_404(request, project_id): + """ + Returns a project or 404s if it doesn't exist. + """ + + # Ensure that a connection is never attempted for a user that is unauthenticated. + if not request.user.is_authenticated: + raise PermissionDenied('User not authenticated') + + nova = get_nova_admin_connection() + project = nova.get_project(project_id) + region = get_current_region(request) + + if not project: + raise Http404('Project %s does not exist.' % project_id) + + return manager.ProjectManager(request.user, project, region) + + +@wrap_nova_error +def get_projects(user): + """ + Returns a list of projects for a user. + """ + #key = 'projects.%s' % user + #projects = cache.get(key) + + #if not projects: + # nova = get_nova_admin_connection() + # projects = nova.get_projects(user=user) + # cache.set(key, projects, 30) + + #return projects + nova = get_nova_admin_connection() + return nova.get_projects(user=user) + + +@wrap_nova_error +def get_all_regions(): + """ + Returns a list of all regions. + """ + regions = cache.get('regions') + + if not regions: + nova = get_nova_admin_connection() + conn = nova.connection_for(settings.NOVA_ADMIN_USER, settings.NOVA_PROJECT) + results = conn.get_all_regions() + regions = [{'name': r.name, 'endpoint': r.endpoint} for r in results] + cache.set('regions', regions, 60 * 60 * 24) + + return regions + + +def get_region(region_name): + regions = get_all_regions() + try: + return [r for r in regions if r['name'] == region_name][0] + except IndexError: + return None + + +def get_current_region(request): + """ + Returns the currently selected region for a user. + """ + region_name = request.session.get('region', settings.NOVA_DEFAULT_REGION) + return get_region(region_name) + + +def set_current_region(request, region_name): + """ + Sets the current region selection for a user. + """ + request.session['region'] = region_name + + +@wrap_nova_error +def get_user_image_permissions(username, project_name): + """ + Returns true if user is a sysadmin and can modify image attributes. + """ + nova = get_nova_admin_connection() + user_has_modify_permissions = False + + # checks global roles, if user is a sysadmin they can modify image attribtues. + if not user_has_modify_permissions: + for role in nova.get_user_roles(username): + if role.role == "sysadmin": + user_has_modify_permissions = True + + # checks project roles, if user is a sysadmin they can modify image attribtues. + if not user_has_modify_permissions: + for role in nova.get_user_roles(username, project_name): + if role.role == "sysadmin": + user_has_modify_permissions = True + + return user_has_modify_permissions + diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/add_project.html b/django-nova/src/django_nova/templates/admin/django_nova/project/add_project.html new file mode 100644 index 00000000..e91d1889 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/add_project.html @@ -0,0 +1,45 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% load admin_modify adminmedia %} + +{% block extrahead %} +{{ block.super }} +{{ media }} +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block bodyclass %} change-form{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="../../projects">Projects</a> › + Add Project +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + {% block object-tools %} + {% endblock %} + <form action="." method="post" enctype="multipart/form-data"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + </fieldset> + <input type="submit" value="Save" /> + </form> +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/add_project_user.html b/django-nova/src/django_nova/templates/admin/django_nova/project/add_project_user.html new file mode 100644 index 00000000..4790a3ca --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/add_project_user.html @@ -0,0 +1,69 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% load admin_modify adminmedia %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + +<script type="text/javascript" src="/media/admin/js/jquery.min.js"></script> +<script type="text/javascript" src="/media/admin/js/jquery.init.js"></script> + +<script type="text/javascript" src="/media/dashboard/js/django-admin.multiselect.js"></script> +<link rel="stylesheet" type="text/css" href="/media/dashboard/css/django-admin-widgets.css" /> + +<script type="text/javascript" charset="utf-8"> + django.jQuery(function(){ + django.jQuery.each(django.jQuery(".edit_user_roles select[multiple]"), function () { + // "Locations" can be any label you want + SelectFilter.init(this.id, "Roles", 0, "/media/admin/"); + }); + }) +</script> +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block bodyclass %} change-form{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="{% url admin_projects %}">Projects</a> › + <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › + User + {{form.ProjectUserForm}} +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + {% block object-tools %} + {% endblock %} + <form class="edit_user_roles" action="." method="post" enctype="multipart/form-data"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + <input type="hidden" name="username" value="{{user.id}}" id="username" /> + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + </fieldset> + <div class="submit-row"> + <p class="deletelink-box"> + <a href="{% url admin_project_delete_user project.projectname user.username %}" class="deletelink">Delete</a> + </p> + <input type="submit" value="Save" class="default" /> + </div> + + </form> +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/base_projects.html b/django-nova/src/django_nova/templates/admin/django_nova/project/base_projects.html new file mode 100644 index 00000000..60759c6b --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/base_projects.html @@ -0,0 +1,16 @@ +{% extends "admin/change_list.html" %} + +{% block extrastyle %} + {{block.super}} + <link rel="stylesheet" type="text/css" href="{{settings.MEDIA_URL}}/stylesheets/extra_admin.css" /> +{% endblock %} +{% block breadcrumbs %}<div class="breadcrumbs"><a href="/admin/">Home</a> › Projects</div>{% endblock %} +{% block content %} + <div id="content-main"> + <div class="module filtered" id="changelist"> + <div id="toolbartable"> + {% block innercontent %}{% endblock %} + </div> + </div> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/change_list.html b/django-nova/src/django_nova/templates/admin/django_nova/project/change_list.html new file mode 100644 index 00000000..ba0046d7 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/change_list.html @@ -0,0 +1,3 @@ +{% extends "admin/change_list.html" %} +{% load admin_extras %} +{% block result_list %}{% project_result_list cl %}{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project.html b/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project.html new file mode 100644 index 00000000..53f180ef --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project.html @@ -0,0 +1,25 @@ +{% extends "admin/change_list.html" %} + +{% block extrastyle %} + {{block.super}} + <link rel="stylesheet" type="text/css" href="{{settings.MEDIA_URL}}/stylesheets/extra_admin.css" /> +{% endblock %} +{% block breadcrumbs %}<div class="breadcrumbs"><a href="/admin/">Home</a> › <a href="/admin/projects">Projects</a> › <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › Delete</div>{% endblock %} +{% block content %} + <div id="content-main"> + <div class="module filtered" id="changelist"> + <div id="toolbartable"> + <h1>Delete Project</h1> + <p>Do you really want to delete this project?</p> + <ul> + <li><a href="{% url admin_project project.projectname %}">{{project.projectname}}</a></li> + </ul> + + <form action="." method="post"> + {% csrf_token %} + <p><input type="submit" value="Delete"></p> + </form> + </div> + </div> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project_user.html b/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project_user.html new file mode 100644 index 00000000..37071ee2 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/delete_project_user.html @@ -0,0 +1,25 @@ +{% extends "admin/change_list.html" %} + +{% block extrastyle %} + {{block.super}} + <link rel="stylesheet" type="text/css" href="{{settings.MEDIA_URL}}/stylesheets/extra_admin.css" /> +{% endblock %} +{% block breadcrumbs %}<div class="breadcrumbs"><a href="/admin/">Home</a> › <a href="/admin/projects">Projects</a> › <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › Delete</div>{% endblock %} +{% block content %} + <div id="content-main"> + <div class="module filtered" id="changelist"> + <div id="toolbartable"> + <h1>Remove User From Project</h1> + <p>Do you really want to remove this user from project?</p> + <ul> + <li><a href="{% url project_user project.projectname user.username %}">{{user.username}}</a> from <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a></li> + </ul> + + <form action="." method="post"> + {% csrf_token %} + <p><input type="submit" value="Delete"></p> + </form> + </div> + </div> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/edit_project.html b/django-nova/src/django_nova/templates/admin/django_nova/project/edit_project.html new file mode 100644 index 00000000..6952eb45 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/edit_project.html @@ -0,0 +1,95 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% load admin_modify adminmedia %} + +{% block extrahead %} +{{ block.super }} +{{ media }} +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block bodyclass %} change-form{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="{% url admin_projects %}">Projects</a> › + <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › + Edit Project + {{form.ProjectEditForm}} +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + {% block object-tools %} + {% endblock %} + <form action="#" method="post" enctype="multipart/form-data"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + <div class="form-row"> + <label for="project_name">Project Name</label> + <span id="project_name" style="display: block; padding-top: 5px; padding-left: 5px; float: left;"><em>{{projectname}}</em></span> + </div> + <div class="form-row"> + <label for="project_description">Description</label> + <span id="project_description" style="display: block; padding-top: 5px; padding-left: 5px; float: left;"><em>{{description}}</em></span> + </div> + <div class="form-row"> + <label for="project_manager">Project Manager</label> + <span id="project_manager" style="display: block; padding-top: 5px; padding-left: 5px; float: left;"><em>{{manager}}</em></span> + </div> + </fieldset> + <div class="submit-row"> + <p class="deletelink-box"> + <a href="{% url delete_project project.projectname %}" class="deletelink">Delete Project</a> + </p> + {# <input type="submit" value="Save" class="default" /> #} + </div> + + </form> + + + <table cellspacing="0" style="margin-top: 20px;"> + <thead> + <tr> + <th>Username</th> + <th>Project Roles</th> + <th>Global Roles</th> + </tr> + </thead> + {% for user in users %} + <tr class="{% cycle 'row1' 'row2' %}"> + <td> + <a href="{%url project_user project.projectname user.memberId %}">{{user.memberId}} {% if user.memberId == project.projectManagerId %}(<em>project manager</em>){%endif %}</a> + + </td> + <td> + {{user.project_roles}} + </td> + <td> + {{user.global_roles}} + </td> + </tr> + {% endfor %} + </table> + <ul class="object-tools"> + <li> + <a class="addlink" href="{% url add_project_user project.projectname %}">Add User</a> + </li> + </ul> + +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/global_edit_user.html b/django-nova/src/django_nova/templates/admin/django_nova/project/global_edit_user.html new file mode 100644 index 00000000..d6d618af --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/global_edit_user.html @@ -0,0 +1,71 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% load admin_modify adminmedia %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + +<script type="text/javascript" src="/media/admin/js/jquery.min.js"></script> +<script type="text/javascript" src="/media/admin/js/jquery.init.js"></script> + +<script type="text/javascript" src="/media/dashboard/js/django-admin.multiselect.js"></script> +<link rel="stylesheet" type="text/css" href="/media/dashboard/css/django-admin-widgets.css" /> + +<script type="text/javascript" charset="utf-8"> + django.jQuery(function(){ + django.jQuery.each(django.jQuery("#global_users select"), function () { + // "Locations" can be any label you want + SelectFilter.init(this.id, "Roles", 0, "/media/admin/"); + }); + }) +</script> +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block bodyclass %} change-form{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="{% url admin_users_list %}">Global Roles</a> › + {{user.username}} +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + {% block object-tools %} + {% endblock %} + <form action="." method="post" enctype="multipart/form-data" id="global_users"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + <div class="form-row"> + <label for="id_username">Username</label> + <span>{{user.username}}</span> + </div> + <input type="hidden" name="username" value="{{user.id}}" id="username" /> + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + </fieldset> + <div class="submit-row"> + <p class="deletelink-box"> + {# <a href="{% url admin_project_delete_user project.projectname user.username %}" class="deletelink">Delete</a> #} + </p> + <input type="submit" value="Save" class="default" /> + </div> + + </form> +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/project_list.html b/django-nova/src/django_nova/templates/admin/django_nova/project/project_list.html new file mode 100644 index 00000000..2159d777 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/project_list.html @@ -0,0 +1,42 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% block extrahead %} + {{ block.super }} +{% endblock %} +{% block innercontent %} + <ul class="object-tools"> + <li> + <a class="addlink" href="{% url add_project %}">Add Project</a> + </li> + </ul> + <table cellspacing="0" style="margin-top: 20px;"> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + <th>Project Manager</th> + <th>Send Credentials</th> + <th>Start VPN</th> + </tr> + </thead> + {% for project in projects %} + <tr class="{% cycle 'row1' 'row2' %}"> + <td> + <a href="{%url admin_project project.projectname %}">{{project.projectname}}</a> + </td> + <td> + {{project.description}} + </td> + <td> + {{project.projectManagerId}} + </td> + <td> + <a href="{% url admin_project_sendcredentials project.projectname %}">Send Credentials</a> + </td> + <td> + <a href="{% url admin_project_start_vpn project.projectname %}">Start VPN</a> + </td> + + </tr> + {% endfor %} + </table> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/project_user.html b/django-nova/src/django_nova/templates/admin/django_nova/project/project_user.html new file mode 100644 index 00000000..7b19564c --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/project_user.html @@ -0,0 +1,76 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% load admin_modify adminmedia %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + +<script type="text/javascript" src="/media/admin/js/jquery.min.js"></script> +<script type="text/javascript" src="/media/admin/js/jquery.init.js"></script> + +<script type="text/javascript" src="/media/dashboard/js/django-admin.multiselect.js"></script> +<link rel="stylesheet" type="text/css" href="/media/dashboard/css/django-admin-widgets.css" /> + +<script type="text/javascript" charset="utf-8"> + django.jQuery(function(){ + django.jQuery.each(django.jQuery(".edit_user_roles select[multiple]"), function () { + // "Locations" can be any label you want + SelectFilter.init(this.id, "Roles", 0, "/media/admin/"); + }); + }) +</script> +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block bodyclass %} change-form{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="{% url admin_projects %}">Projects</a> › + <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › + User + {{form.ProjectUserForm}} +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + {% block object-tools %} + {% endblock %} + <form class="edit_user_roles" action="." method="post" enctype="multipart/form-data"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + <div class="form-row"> + <label for="id_username">Username</label> + <span>{{user.username}}</span> + </div> + <input type="hidden" name="username" value="{{user.id}}" id="username" /> + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + </fieldset> + <div class="submit-row"> + {% if project.projectManagerId != user.username %} + <p class="deletelink-box"> + <a href="{% url admin_project_delete_user project.projectname user.username %}" class="deletelink">Remove User From Project</a> + </p> + {% endif %} + + <input type="submit" value="Save" class="default" /> + </div> + + </form> +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/send_credentials.html b/django-nova/src/django_nova/templates/admin/django_nova/project/send_credentials.html new file mode 100644 index 00000000..1bd0294e --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/send_credentials.html @@ -0,0 +1,87 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_modify adminmedia %} + +{% block title %}Send project credentials{{ block.super }}{% endblock %} + +{% block extrahead %} +{{ block.super }} +{{ media }} + +<script type="text/javascript" src="/media/admin/js/jquery.min.js"></script> +<script type="text/javascript" src="/media/admin/js/jquery.init.js"></script> + +<script type="text/javascript" src="/media/dashboard/js/django-admin.multiselect.js"></script> +<link rel="stylesheet" type="text/css" href="/media/dashboard/css/django-admin-widgets.css" /> + +<script type="text/javascript" charset="utf-8"> + django.jQuery(function(){ + django.jQuery.each(django.jQuery("#send_credentials select"), function () { + // "Locations" can be any label you want + SelectFilter.init(this.id, "Users", 0, "/media/admin/"); + }); + }) +</script> + +<style type="text/css" media="screen"> + .errorlist, .successlist {background:#fcc;border:1px solid #c66;color:#600;list-style:none; padding: 10px 5px; margin: 25px 0 25px 0; float: left; width: 100%;} + .successlist {background: #CBFBD7; color: #1E5024; border-color: #6FBA5C;} +</style> +{% endblock %} + + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + <a href="{% url admin_projects %}">Projects</a> › + <a href="{% url admin_project project.projectname %}">{{project.projectname}}</a> › + Send Credentials +</div> +{% endblock %} + +{% block content %} +<div id="content-main"> + + + {% if not success %} + <h1>Send Credentials</h1> + <h3>Select which users you would like to send credentials to from the '{{ project.projectname }}' project.</h3> + {% else %} + <h1>Credentials sent successfully</h1> + {% endif %} + + <div class="status"> + {% if error %} + <span class="errorlist">{{ error }}</span> + {% endif %} + + {% if success %} + <span class="successlist">{{ success }}</span> + {% endif %} + + </div> + + {% if not success %} + <form id="send_credentials" action="{% url admin_project_sendcredentials project.projectname %}" method="post"> + {% csrf_token %} + <fieldset class="module aligned"> + + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + </fieldset> + <div class="submit-row"> + <input style="margin-top:20px; margin-left:10px;" type="submit" value="Send Credentials" /> + </div> + + </form> + {% endif %} +</div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/admin/django_nova/project/user_list.html b/django-nova/src/django_nova/templates/admin/django_nova/project/user_list.html new file mode 100644 index 00000000..4bd59427 --- /dev/null +++ b/django-nova/src/django_nova/templates/admin/django_nova/project/user_list.html @@ -0,0 +1,39 @@ +{% extends "admin/django_nova/project/base_projects.html" %} +{% block extrahead %} + {{ block.super }} +{% endblock %} + +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="/admin">Home</a> › + Global Roles +</div> +{% endblock %} + +{% block innercontent %} + <h1>Select a User</h1> + + <table cellspacing="0" style="margin-top: 20px;"> + <thead> + <tr> + <th>Username</th> + <th>Global Roles</th> + <th>Actions</th> + </tr> + </thead> + {% for user in users %} + <tr class="{% cycle 'row1' 'row2' %}"> + <td> + {{user.username}} + </td> + <td> + (temporarily hidden) + {#user.roles#} + </td> + <td> + <a href="{%url admin_user_roles user.username %}">Edit</a> + </td> + </tr> + {% endfor %} + </table> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/_messages.html b/django-nova/src/django_nova/templates/django_nova/_messages.html new file mode 100644 index 00000000..8cead94b --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/_messages.html @@ -0,0 +1,41 @@ +{% for message in messages %} + <div class="message ui-widget"> + {% if message.tags == "info" %} + <div class="ui-state-highlight ui-corner-all"> + <span class="close ui-icon ui-icon-circle-close"></span> + <p> + <span class="ui-icon ui-icon-info"></span> + {{ message }} + </p> + </div> + {% endif %} + {% if message.tags == "warning" %} + <div class="ui-state-highlight ui-corner-all" > + <span class="close ui-icon ui-icon-circle-close"></span> + <p> + <span class="ui-icon ui-icon-alert"></span> + {{ message }} + </p> + </div> + {% endif %} + {% if message.tags == "success" %} + <div class="ui-state-highlight ui-corner-all success" > + <span class="close ui-icon ui-icon-circle-close"></span> + <p> + <span class="ui-icon ui-icon-check"></span> + {{ message }} + </p> + </div> + {% endif %} + {% if message.tags == "error" %} + <div class="ui-state-error ui-corner-all" > + <span class="close ui-icon ui-icon-circle-close"></span> + <p> + <span class="ui-icon ui-icon-alert"></span> + {{ message }} + </p> + </div> + {% endif %} + </div> +{% endfor %} + diff --git a/django-nova/src/django_nova/templates/django_nova/base.html b/django-nova/src/django_nova/templates/django_nova/base.html new file mode 100644 index 00000000..d5551ae9 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/base.html @@ -0,0 +1,85 @@ +{% extends "base-sidebar.html" %} +{% load region_tags %} +{% load project_tags %} + +{% block headerjs %} +{{ block.super }} +{% endblock %} + +{% block region %} + <div id="region_selector"> + {% load_regions %} + <span id="project_name"><strong>Project:</strong> {{ project.projectname }}</span> + + <form id="frm_region" method="post" action="{% url region_change %}"> + {% csrf_token %} + <fieldset> + <input name="redirect_url" type="hidden" value="{{ request.get_full_path }}" /> + <noscript> + <input id="btn_region_change" type="submit" value="Change" /> + </noscript> + + <div id="region_form"> + <label for="sel_region">Region: </label> + <select id="sel_region" name="region"> + {% for region in regions %} + <option{% if region.name == current_region.name %} selected{% endif %}> + {{ region.name }} + </option> + {% endfor %} + </select> + </div> + + </fieldset> + </form> + </div> +{% endblock %} + +{% block nav_projects %} + {% load_projects %} + <li> + <h3 class="active"><a href="/">Projects</a></h3> + <div id="projects"> + {% for p in projects %} + <div id="{{ p.projectname }}" class="project{% if p.projectname == project.projectname %} active{% endif %}"> + <h4> + <a class="project_link" href="/project/{{ p.projectname }}">{{ p.projectname }}</a> + {% if p.projectManagerId == user.username %} + <a id="manage_project_{{p.projectname}}" + class="manage_link" + href="{% url nova_project_manage p.projectname %}" + title="Manage User Roles">Manage Project</a> + {% endif %} + </h4> + {% if project.projectname == p.projectname %} + <ul> + <li {% if p.projectname == project.projectname and sidebar_selected == "instances" %}class="active"{% endif %}> + <a id="lnk_instances_{{p.projectname}}" href="{% url nova_instances p.projectname %}">Instances</a> + </li> + <li {% if p.projectname == project.projectname and sidebar_selected == "images" %}class="active"{% endif %}> + <a id="lnk_images_{{p.projectname}}" href="{% url nova_images p.projectname %}">Images</a> + </li> + <li {% if p.projectname == project.projectname and sidebar_selected == "keys" %}class="active"{% endif %}> + <a id="lnk_keypairs_{{p.projectname}}" href="{% url nova_keypairs p.projectname %}">Keys</a> + </li> + <li {% if p.projectname == project.projectname and sidebar_selected == "volumes" %}class="active"{% endif %}> + <a id="lnk_volumes_{{p.projectname}}" href="{% url nova_volumes p.projectname %}">Volumes</a> + </li> + </ul> + {% endif %} + </div> + {% endfor %} + </div> + </li> +{% endblock %} + +{% block footerjs %} +{{ block.super }} +<script type="text/javascript"> + $(function() { + $('#sel_region').change(function() { + $('#frm_region').submit(); + }); + }); +</script> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/credentials/expired.html b/django-nova/src/django_nova/templates/django_nova/credentials/expired.html new file mode 100644 index 00000000..7fd741ff --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/credentials/expired.html @@ -0,0 +1,17 @@ +{% load django_nova_tags %} +<!DOCTYPE html> +<html lang="en" xml:lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <title>Expired Token</title> + </head> + <body> + <center> + <h1>The link you clicked has expired.</h1> + <p style="width:460px;">This credentials download link you have reached + is either invalid or has expired. Each link is only good for one use. If + you need to download your credentials again, please contact the + {% site_branding %} support team.</p> + </center> + </body> +</html> diff --git a/django-nova/src/django_nova/templates/django_nova/images/_launch_form.html b/django-nova/src/django_nova/templates/django_nova/images/_launch_form.html new file mode 100644 index 00000000..3d5a01e5 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/_launch_form.html @@ -0,0 +1,7 @@ +{% for field in form %} +<div class="{% cycle 'odd' 'even'%}"> + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} +</div> +{% endfor %} diff --git a/django-nova/src/django_nova/templates/django_nova/images/_list.html b/django-nova/src/django_nova/templates/django_nova/images/_list.html new file mode 100644 index 00000000..ec25c521 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/_list.html @@ -0,0 +1,112 @@ + <h3 class="image_list_heading"> {{ heading }} </h3> + {% if images %} + <table id="image_launch"> + <tr> + <th>ID</th> + <th>Description</th> + <th colspan="2">Owner</th> + </tr> + {% for image in images %} + {% if image.id == ami.id %} + <td class="detail_wrapper" colspan="4"> + <div id="{{ ami.id }}" class="image_detail"> + <div class="column"> + <div class="image_detail_item"> + <span class="label">Owner: </span> + <span class="data">{{ ami.ownerId }}</span> + </div> + + <div class="image_detail_item"> + <span class="label">Description: </span> + <span class="data">{{ ami.description }}</span> + </div> + + <div class="image_detail_item"> + <span class="label">Location: </span> + <span class="data">{{ ami.location }}</span> + </div> + </div> + + <div class="column"> + <div class="image_detail_item"> + <span class="label">ID: </span> + <span class="data">{{ ami.id }}</span> + </div> + <div class="image_detail_item"> + <span class="label">Name: </span> + <span class="data">{% if ami.displayName %}{{ ami.displayName }}{%else%}{{ ami.id }}{% endif %}</span> + </div> + <div class="image_detail_item"> + <span class="label">Type: </span> + <span class="data">{{ ami.type }}</span> + </div> + <div class="image_detail_item"> + <span class="label">Architecture: </span> + <span class="data">{{ ami.architecture }}</span> + </div> + </div> + + <div id="last" class="column"> + {% if ami.is_public %} + <div id="public" class="privacy">Public Image</div> + {% else %} + <div id="private" class="privacy">Private Image</div> + {% endif %} + + <a id="launch_{{ image.id }}" class="launch" href="{% url nova_images_launch project.projectname image.id %}" title="Click to launch image">Launch</a> + {% if can_modify or user.username == ami.ownerId %} + <a id="edit_image_link" href="{% url nova_images_update project.projectname ami.id %}">Edit Image</a> + {% endif %} + + </div> + + {% if can_modify or user.username == ami.ownerId %} + <span class="image_privacy"> + <form id="privacy_{{ ami.id }}" action="{% url nova_images_privacy project.projectname ami.id %}" method="post" accept-charset="utf-8"> + {% csrf_token %} + {% if ami.is_public %} + <input class="private" type="submit" value="Make Private" /> + {% else %} + <input class="public" type="submit" value="Make Public" /> + {% endif %} + </form> + </span> + + <span class="delete"> + <form id="delete_{{ ami.id }}" action="{% url nova_images_remove project.projectname ami.id %}" method="post" accept-charset="utf-8"> + {% csrf_token %} + <input type="submit" value="Remove Image" /> + </form> + </span> + {% endif %} + </div> + </td> + {% else %} + <tr class="{% cycle 'odd' 'even' %}"> + <td class="image_id"> + <a href="{% url nova_images_detail project.projectname image.id %}">{% if image.displayName %}{{ image.displayName }}{%else%}{{ image.id }}{% endif %}</a> + </td> + <td class="image_location odd"> + {% if image.description %} + {{ image.description }} + {% else %} + {{ image.location }} + {% endif %} + </td> + <td class="image_owner_id">{{ image.ownerId }}</td> + <td class="image_launch_btn odd"><a id="launch_{{ image.id }}" class="launch" href="{% url nova_images_launch project.projectname image.id %}">Launch</a></td> + {#<td class="odd"><a class="ui-state-default ui-corner-all" onclick="$('#dlg_launch').dialog('open');">Launch</a></td>#} + </tr> + {% endif %} + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No images currently available. + </p> + </div> + </div> + {% endif %} diff --git a/django-nova/src/django_nova/templates/django_nova/images/base.html b/django-nova/src/django_nova/templates/django_nova/images/base.html new file mode 100644 index 00000000..e9649723 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/base.html @@ -0,0 +1,7 @@ +{% extends "django_nova/base.html" %} +{% load sidebar_tags %} + +{% block nav_projects %} + {% sidebar_select images %} + {{ block.super }} +{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/images/detail_list.html b/django-nova/src/django_nova/templates/django_nova/images/detail_list.html new file mode 100644 index 00000000..9c979e2b --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/detail_list.html @@ -0,0 +1,207 @@ +{% extends "django_nova/images/base.html" %} + +{% block title %} - Launch an Image{% endblock %} + +{% block headerjs %} +<script type="text/javascript" src="/media/django_nova/js/jquery.form.js"></script> +{% endblock %} + + +{% block content %} + <div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Images</h2> + <p id="page_description">Images are snapshots of running systems which can easily be deployed to run one or more instances.</p> + </div> + + {% include "django_nova/_messages.html" %} + + {% if images %} + <table id="image_launch"> + <tr> + <th>ID</th> + <th>Description</th> + <th colspan="2">Owner</th> + </tr> + {% for image in images %} + <tr class="{% cycle 'odd' 'even' %}"> + {% if image.id == ami.id %} + <td class="detail_wrapper" colspan="4"> + <div id="{{ ami.id }}" class="image_detail"> + <div class="column"> + <div class="image_detail_item"> + <span class="label">Owner: </span> + <span class="data">{{ ami.ownerId }}</span> + </div> + + <div class="image_detail_item"> + <span class="label">Description: </span> + <span class="data">{{ ami.description }}</span> + </div> + + <div class="image_detail_item"> + <span class="label">Location: </span> + <span class="data">{{ ami.location }}</span> + </div> + </div> + + <div class="column"> + <div class="image_detail_item"> + <span class="label">ID: </span> + <span class="data">{{ ami.id }}</span> + </div> + <div class="image_detail_item"> + <span class="label">Name: </span> + <span class="data">{% if ami.displayName %}{{ ami.displayName }}{%else%}{{ ami.id }}{% endif %}</span> + </div> + <div class="image_detail_item"> + <span class="label">Type: </span> + <span class="data">{{ ami.type }}</span> + </div> + <div class="image_detail_item"> + <span class="label">Architecture: </span> + <span class="data">{{ ami.architecture }}</span> + </div> + </div> + + <div id="last" class="column"> + {% if ami.is_public %} + <div id="public" class="privacy">Public Image</div> + {% else %} + <div id="private" class="privacy">Private Image</div> + {% endif %} + + <a id="launch_{{ image.id }}" class="launch" href="{% url nova_images_launch project.projectname image.id %}" title="Click to launch image">Launch</a> + {% if can_modify or user.username == ami.ownerId %} + <a id="edit_image_link" href="{% url nova_images_update project.projectname ami.id %}">Edit Image</a> + {% endif %} + + </div> + + {% if can_modify or user.username == ami.ownerId %} + <span class="image_privacy"> + <form id="privacy_{{ ami.id }}" action="{% url nova_images_privacy project.projectname ami.id %}" method="post" accept-charset="utf-8"> + {% csrf_token %} + {% if ami.is_public %} + <input class="private" type="submit" value="Make Private" /> + {% else %} + <input class="public" type="submit" value="Make Public" /> + {% endif %} + </form> + </span> + + <span class="delete"> + <form id="delete_{{ ami.id }}" action="{% url nova_images_remove project.projectname ami.id %}" method="post" accept-charset="utf-8"> + {% csrf_token %} + <input type="submit" value="Remove Image" /> + </form> + </span> + {% endif %} + </div> + </td> + {% else %} + <td class="image_id"><a href="{% url nova_images_detail project.projectname image.id %}">{{ image.id }}</a></td> + <td class="image_location odd"> + {% if image.description %} + {{ image.description }} + {% else %} + {{ image.location }} + {% endif %} + </td> + <td class="image_owner_id">{{ image.ownerId }}</td> + <td class="image_launch_btn odd"><a id="launch_{{ image.id }}" class="launch" href="{% url nova_images_launch project.projectname image.id %}">Launch</a></td> + {#<td class="odd"><a class="ui-state-default ui-corner-all" onclick="$('#dlg_launch').dialog('open');">Launch</a></td>#} + {% endif %} + </tr> + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No images currently available. + </p> + </div> + </div> + {% endif %} + </div> + + <div id="dlg_launch" title="Launch Instance" style="display:none;"> + <form id="frm_launch" action="url nova_images_launch project.projectname" method="post"> + {% csrf_token %} + {% include "django_nova/images/_launch_form.html" %} + </form> + </div> + + <div id="dlg_confirm" title="Confirm Termination"> + <p>Are you sure you wish to unregister the <span id="ami_name"></span> image?</p> + </div> + +{% endblock %} + +{% block footerjs %} +{{ block.super }} +<script type="text/javascript"> + var options = { + success: handleResponse, + beforeSubmit: showRequest, + dataType: 'json' + } + + // TODO: On dialog open, reset form and validation. + $(function() { + $('#dlg_launch').dialog({ + buttons: { + 'Ok': function() { + $('#frm_launch').ajaxSubmit(options); + }, + 'Cancel': function() { + $(this).dialog('close'); + } + }, + autoOpen: false, + resizable: false, + width: 400, + height: 400 + }); + }); + + function showRequest(formData, jqForm, options) { + var queryString = $.param(formData); + alert('About to submit: \n\n' + queryString); + return true; + } + + function handleResponse(data, statusText, xhr, $form) { + alert('status: ' + statusText + '\nsuccess:\n\n' + data.success); + } + + $(function(){ + $('.delete form').submit(function() { + ami_name = $(this).parent().parent().attr("id"); + $('#ami_name').text(ami_name); + $('#dlg_confirm').dialog('open'); + return false; + }); + + $('#dlg_confirm').dialog({ + buttons: { + 'Ok': onConfirmOK, + 'Cancel': function() { $(this).dialog('close'); } + }, + autoOpen: false, + resizable: false, + width: 500, + height: 200 + }); + }) + + function onConfirmOK() { + $(this).dialog('close'); + form = document.getElementById('delete_' + ami_name); + if(form) form.submit(); + } + +</script> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/images/edit.html b/django-nova/src/django_nova/templates/django_nova/images/edit.html new file mode 100644 index 00000000..800cfbb3 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/edit.html @@ -0,0 +1,35 @@ +{% extends "django_nova/images/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block headerjs %} +<script type="text/javascript" src="{{ COMMON_MEDIA_PREFIX }}js/jquery.form.js"></script> +{% endblock %} + +{% block content %} +<div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Edit Image</h2> + <p id="page_description">From this page you can edit the name and description of an image that belongs to you.</p> + </div> + + <div class="dash_block first"> + <h3 class="image_id">Edit Image: {{ ami.id }}</h3> + + <form class="edit_image" id="rename_{{ ami.id }}" action="{% url nova_images_update project.projectname ami.id %}" method="post"> + <fieldset> + {% csrf_token %} + {% for field in form %} + + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} + {% endfor %} + <input type="submit" value="Update Image" /> + </fieldset> + </form> + + </div> +</div> + +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/images/index.html b/django-nova/src/django_nova/templates/django_nova/images/index.html new file mode 100644 index 00000000..913bf012 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/index.html @@ -0,0 +1,70 @@ +{% extends "django_nova/images/base.html" %} + +{% block title %} - Launch an Image{% endblock %} + +{% block headerjs %} +<script type="text/javascript" src="/media/django_nova/js/jquery.form.js"></script> +{% endblock %} + + +{% block content %} + <div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Images</h2> + <p id="page_description">Images are snapshots of running systems which can easily be deployed to run one or more instances.</p> + </div> + + {% include "django_nova/_messages.html" %} + + {% for heading, images in image_lists.items %} + {% include "django_nova/images/_list.html" %} + {% endfor %} + + </div> + + <div id="dlg_launch" title="Launch Instance" style="display:none;"> + <form id="frm_launch" action="#" method="post"> + {% csrf_token %} + {% include "django_nova/images/_launch_form.html" %} + </form> + </div> +{% endblock %} + +{% block footerjs %} +{{ block.super }} +<script type="text/javascript"> + var options = { + success: handleResponse, + beforeSubmit: showRequest, + dataType: 'json' + } + + // TODO: On dialog open, reset form and validation. + $(function() { + $('#dlg_launch').dialog({ + buttons: { + 'Ok': function() { + $('#frm_launch').ajaxSubmit(options); + }, + 'Cancel': function() { + $(this).dialog('close'); + } + }, + autoOpen: false, + resizable: false, + width: 400, + height: 400 + }); + }); + + function showRequest(formData, jqForm, options) { + var queryString = $.param(formData); + alert('About to submit: \n\n' + queryString); + return true; + } + + function handleResponse(data, statusText, xhr, $form) { + alert('status: ' + statusText + '\nsuccess:\n\n' + data.success); + } +</script> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/images/launch.html b/django-nova/src/django_nova/templates/django_nova/images/launch.html new file mode 100644 index 00000000..6e3990e7 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/images/launch.html @@ -0,0 +1,32 @@ +{% extends "django_nova/images/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block headerjs %} +<script type="text/javascript" src="{{ COMMON_MEDIA_PREFIX }}js/jquery.form.js"></script> +{% endblock %} + +{% block content %} +<div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Launch Image</h2> + <p id="page_description">You can launch up to five instances of an image at a time. Some images allow for custom configuration to be passed in via User data. (<a href="/kb/show/UserData/">read more</a>)</p> + </div> + + <div class="dash_block first"> + <form id="frm_launch" action="{% url nova_images_launch project.projectname ami.id %}" method="post"> + {% csrf_token %} + <fieldset> + <h3 class="image_id">Launch Image {{ ami.id }}</h3> + <div class="even"> + <label>Location</label> + <span class="image_location">{{ ami.location }}</span> + </div> + {% include "django_nova/images/_launch_form.html" %} + <input id="launch_image" type="submit" value="Launch" /> + </fieldset> + </form> + </div> +</div> + +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/instances/_instances_list.html b/django-nova/src/django_nova/templates/django_nova/instances/_instances_list.html new file mode 100644 index 00000000..f6b9f283 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/_instances_list.html @@ -0,0 +1,104 @@ + {% if instances %} + <table style="width: 100%"> + <tr> + <th>ID</th> + <th>Image</th> + <th>Size</th> + <th>IP</th> + <th colspan="2">State</th> + </tr> + {% for instance in instances %} + <tr class="{% cycle 'odd' 'even' %}"> + {% if instance.id == selected_instance.id %} + <td class="detail_wrapper" colspan="6"> + <div id="{{selected_instance.id}}" class="instance_detail"> + <div class="column"> + <div class="instance_detail_item"> + <span class="label">Instance ID: </span> + <span class="data">{{ selected_instance.id }}</span> + </div> + <div class="instance_detail_item"> + <span class="label">Name: </span> + <span class="data">{% if selected_instance.displayName != "" %}{{ selected_instance.displayName }}{% else %} None {% endif %}</span> + </div> + <div class="instance_detail_item"> + <span class="label">Description: </span> + <span class="data" id="desc">{{ selected_instance.displayDescription }}</span> + </div> + </div> + + <div class="column"> + <div class="instance_detail_item"> + <span class="label">Region: </span> + <span class="data">{{ selected_instance.region.name }}</span> + </div> + <div class="instance_detail_item"> + <span class="label">Size: </span> + <span class="data">{{ selected_instance.instance_type }}</span> + </div> + <div class="instance_detail_item"> + <span class="label">State: </span> + <span class="data">{{ selected_instance.state }}</span> + </div> + <div class="instance_detail_item"> + <span class="label">Image ID: </span> + <span class="data">{{ selected_instance.image_id }}</span> + </div> + <div class="instance_detail_item"> + <span class="label">IP Address: </span> + <span class="data">{{ selected_instance.dns_name }}</span> + </div> + + </div> + + <div id="last" class="column"> + {% if instance.state == "running" %} + <a href="{% url nova_instances_console project.projectname instance.id %}" id="console_{{instance.id}}" class="console" target="_blank">Show Console</a>{% endif %} + <a id="edit_instance_link" href="{% url nova_instance_update project.projectname instance.id %}">Edit Instance</a> + + + </div> + + <span class="delete"> + <form id="form_terminate_{{ instance.id }}" class="form-terminate" method="post" action="{% url nova_instances_terminate project.projectname %}" > + <input name="instance_id" type="hidden" value="{{ instance.id }}" /> + <input id="terminate_{{instance.id}}" class="terminate" type="submit" value="Terminate" /> + {% csrf_token %} + </form> + </span> + + </div> + + </td> + {% else %} + + <td><a href="{% url nova_instances_detail project.projectname instance.id %}">{{ instance.id }} {% if instance.displayName %}({{ instance.displayName }}){% endif %} + </a></td> + <td class="odd">{{ instance.image_id }}</td> + <td>{{ instance.instance_type }}</td> + <td class="odd">{{ instance.dns_name }}</td> + <td>{{ instance.state }}</td> + <td id="actions" class="odd"> + <form id="form_terminate_{{ instance.id }}" class="form-terminate" method="post" action="{% url nova_instances_terminate project.projectname %}" > + <input name="instance_id" type="hidden" value="{{ instance.id }}" /> + <input id="terminate_{{instance.id}}" class="terminate" type="submit" value="Terminate" /> + {% csrf_token %} + </form> + {% if instance.state == "running" %} + <a href="{% url nova_instances_console project.projectname instance.id %}" id="console_{{instance.id}}" class="console" target="_blank">Show Console</a> + {% endif %} + </td> + {% endif %} + </tr> + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No instances are currently running. You may start a new instance from the <a href="{% url nova_images project.projectname %}">images</a> tab. + </p> + </div> + </div> + {% endif %} diff --git a/django-nova/src/django_nova/templates/django_nova/instances/base.html b/django-nova/src/django_nova/templates/django_nova/instances/base.html new file mode 100644 index 00000000..b793e937 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/base.html @@ -0,0 +1,7 @@ +{% extends "django_nova/base.html" %} +{% load sidebar_tags %} + +{% block nav_projects %} + {% sidebar_select instances %} + {{ block.super }} +{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/instances/detail_list.html b/django-nova/src/django_nova/templates/django_nova/instances/detail_list.html new file mode 100644 index 00000000..a3ba8724 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/detail_list.html @@ -0,0 +1,33 @@ +{% extends "django_nova/instances/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block content %} + +<div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Instance ID: {{ instance.id }}</h2> + <p id="page_description">Here you can see up to the minute performance data about your instance.</p> + </div> + + {% include "django_nova/_messages.html" %} + + + <div class="dash_block first"> + <h3 class="image_id">Edit Instance: {{ instance.id }}</h3> + + <form class="edit_instance" id="rename_{{ instance.id }}" action="{% url nova_instance_update project.projectname instance.id %}" method="post"> + <fieldset> + {% csrf_token %} + {% for field in form %} + + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} + {% endfor %} + <input type="submit" value="Update Instance" /> + </fieldset> + </form> + </div> + +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/instances/edit.html b/django-nova/src/django_nova/templates/django_nova/instances/edit.html new file mode 100644 index 00000000..17c987e8 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/edit.html @@ -0,0 +1,34 @@ +{% extends "django_nova/instances/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block headerjs %} +<script type="text/javascript" src="{{ COMMON_MEDIA_PREFIX }}js/jquery.form.js"></script> +{% endblock %} + +{% block content %} +<div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Edit Instance</h2> + <p id="page_description">From this page you can give your instance an alias, so you don't have to remember its unique id.</p> + </div> + + <div class="dash_block first"> + <h3 class="image_id">Edit Instance: {{ instance.id }}</h3> + + <form class="edit_instance" id="rename_{{ selected_instance.id }}" action="{% url nova_instance_update project.projectname instance.id %}" method="post"> + <fieldset> + {% csrf_token %} + {% for field in update_form %} + + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} + {% endfor %} + <input type="submit" value="Update Instance" /> + </fieldset> + </form> + </div> +</div> + +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/instances/index.html b/django-nova/src/django_nova/templates/django_nova/instances/index.html new file mode 100644 index 00000000..4774782a --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/index.html @@ -0,0 +1,98 @@ +{% extends "django_nova/instances/base.html" %} + +{% block title %} - {{ project.projectname|capfirst }} Instances{% endblock %} +{% block pageclass %}instances{% endblock %} + +{% block content %} + <div id="page_head"> + <div id="spinner"></div> + + <h2 id="page_heading">Instances</h2> + <p id="page_description">Instances are virtual servers launched from images. You can launch instances from the <a href="{% url nova_images project.projectname %}">images tab</a>.</p> + </div> + + {% include "django_nova/_messages.html" %} + + <div id="instances"> + {% include "django_nova/instances/_instances_list.html" %} + </div> + + <div id="dlg_confirm" title="Confirm Termination" style="display:none;"> + <p>Are you sure you wish to terminate instance <span id="spn_terminate"></span>?</p> + </div> + + <div id="connection_error" style="display:none;" title="Connection Error"> + <p><span class="ui-icon ui-icon-alert"></span>A connection error has occurred. Please ensure you are still connected to VPN.</p> + </div> +{% endblock %} + +{% block footerjs %} +{{ block.super }} +<script type="text/javascript"> + + $(function() { + setInterval(function() { + $('#spinner').show(); + {% if detail %} + $('#instances').load('{% url nova_instances_refresh_detail project.projectname selected_instance.id %}', onInstancesUpdated); + {% else %} + $('#instances').load('{% url nova_instances_refresh project.projectname %}', onInstancesUpdated); + {% endif %} + }, 15000); + + initInstanceForms(); + + $('#dlg_confirm').dialog({ + buttons: { + 'Ok': onConfirmOK, + 'Cancel': function() { $(this).dialog('close'); } + }, + autoOpen: false, + resizable: false, + width: 500, + height: 200 + }); + }); + + var _terminateID = null; + + function initInstanceForms() { + $('.form-terminate').submit(function() { + _terminateID = $(this).children(':first').val() + $('#spn_terminate').text(_terminateID); + $('#dlg_confirm').dialog('open'); + return false; + }); + } + + function onInstancesUpdated(response, status, xhr) { + $('#spinner').hide(); + + switch(xhr.status) { + case 200: + initInstanceForms(); + break; + + case 403: + document.location = '{% url auth_login %}'; + break; + + default: + $('#connection_error').dialog({ + dialogClass: 'alert', + modal: true, + closeOnEscape: true, + buttons:{ "Close": function() { $(this).dialog("close"); } }, + }); + $('#connection_error').dialog('open'); + break; + } + } + + function onConfirmOK() { + $(this).dialog('close'); + form = document.getElementById('form_terminate_' + _terminateID); + if(form) form.submit(); + } +</script> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/instances/performance.html b/django-nova/src/django_nova/templates/django_nova/instances/performance.html new file mode 100644 index 00000000..7c941ab8 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/instances/performance.html @@ -0,0 +1,58 @@ +{% extends "django_nova/instances/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block content %} + +<div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Instance ID: {{ instance.id }} Performance</h2> + <p id="page_description">Here you can see up to the minute performance data about your instance.</p> + </div> + + {% include "django_nova/_messages.html" %} + + <p> + <h1>CPU Usage</h1> + <h3>Today</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "cpu-1d.png" %}" /> + </p> + <p> + <h3>This Week</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "cpu-1w.png" %}" /> + </p> + <p> + <h3>This Month</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "cpu-1m.png" %}" /> + </p> + + <p> + <h1>Network Activity</h1> + <h3>Today</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "net-1d.png" %}" /> + </p> + <p> + <h3>This Week</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "net-1w.png" %}" /> + </p> + <p> + <h3>This Month</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "net-1m.png" %}" /> + </p> + + <p> + <h1>Disk Activity</h3> + <h3>Today</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "disk-1d.png" %}" /> + </p> + <p> + <h3>This Week</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "disk-1w.png" %}" /> + </p> + <p> + <h3>This Month</h3> + <img src="{% url nova_instances_graph project.projectname instance.id "disk-1m.png" %}" /> + </p> +</div> + +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/keypairs/_create_form.html b/django-nova/src/django_nova/templates/django_nova/keypairs/_create_form.html new file mode 100644 index 00000000..d295c406 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/keypairs/_create_form.html @@ -0,0 +1,5 @@ +{% for field in create_form %} +{{ field.label_tag }} +{% if field.errors %}{{ field.errors }}{% endif %} +{{ field }} +{% endfor %} diff --git a/django-nova/src/django_nova/templates/django_nova/keypairs/_list.html b/django-nova/src/django_nova/templates/django_nova/keypairs/_list.html new file mode 100644 index 00000000..2447a193 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/keypairs/_list.html @@ -0,0 +1,31 @@ + {% if keypairs %} + <table style="width: 100%"> + <tr> + <th>Key Pair Name</th> + <th>Fingerprint</th> + <th> </th> + </tr> + {% for keypair in keypairs %} + <tr class="{% cycle 'odd' 'even' %}"> + <td>{{ keypair.name }}</td> + <td class="odd">{{ keypair.fingerprint }}</td> + <td> + <form id="form_key_delete_{{keypair.name}}" class="form-key-delete" method="post" action="{% url nova_keypairs_delete project.projectname %}"> + <input name="key_name" type="hidden" value="{{ keypair.name }}" /> + <input id="keypair_delete_{{keypair.name}}" class="delete" type="submit" value="Delete" /> + {% csrf_token %} + </form> + </td> + </tr> + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No key pairs currently exist. + </p> + </div> + </div> + {% endif %} diff --git a/django-nova/src/django_nova/templates/django_nova/keypairs/base.html b/django-nova/src/django_nova/templates/django_nova/keypairs/base.html new file mode 100644 index 00000000..5fd996ca --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/keypairs/base.html @@ -0,0 +1,7 @@ +{% extends "django_nova/base.html" %} +{% load sidebar_tags %} + +{% block nav_projects %} + {% sidebar_select keys %} + {{ block.super }} +{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/keypairs/index.html b/django-nova/src/django_nova/templates/django_nova/keypairs/index.html new file mode 100644 index 00000000..2233f481 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/keypairs/index.html @@ -0,0 +1,77 @@ +{% extends "django_nova/keypairs/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block headerjs %} +{{ block.super }} +<script type="text/javascript" src="/media/dashboard/js/jquery.form.js"></script> +{% endblock %} + +{% block content %} + <div id="page_head"> + <h2 id="page_heading">Keys</h2> + <p id="page_description">Key pairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a pem file). <em>Protect and use the key as a normal private key.</em></p> + </div> + + {% include "django_nova/_messages.html" %} + + <div id="instances"> + {% include "django_nova/keypairs/_list.html" %} + </div> + + <div class="dash_block first"> + <form id="frm_key_create" action="{% url nova_keypairs_add project.projectname %}" method="post"> + {% csrf_token %} + <input id="js" name="js" type="hidden" value="0" /> + <fieldset> + <h3>Create New Keypair</h3> + {% include "django_nova/keypairs/_create_form.html" %} + <input id="keypair_create" class="create" type="submit" value="Create" /> + </fieldset> + </form> + </div> + + <div id="dlg_confirm" title="Confirm Termination"> + <p>Are you sure you wish to delete key <span id="spn_delete_key_name"></span>?</p> + </div> +{% endblock %} + +{% block footerjs %} +{{ block.super }} +<script type="text/javascript"> + $(function() { $('#js').val('1'); }); + + {% if download_key %} + $(function() { window.location = '{% url nova_keypairs_download project.projectname download_key %}'; }); + {% endif %} + + $(function() { + $('.form-key-delete').submit(function() { + _key_name = $(this).children(':first').val() + $('#spn_delete_key_name').text(_key_name); + $('#dlg_confirm').dialog('open'); + return false; + }); + + $('#dlg_confirm').dialog({ + buttons: { + 'Ok': onConfirmOK, + 'Cancel': function() { $(this).dialog('close'); } + }, + autoOpen: false, + resizable: false, + width: 500, + height: 200 + }); + + }); + + var _terminateID = null; + + function onConfirmOK() { + $(this).dialog('close'); + form = document.getElementById('form_key_delete_' + _key_name); + if(form) form.submit(); + } +</script> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/projects/edit_user.html b/django-nova/src/django_nova/templates/django_nova/projects/edit_user.html new file mode 100644 index 00000000..44675ce4 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/projects/edit_user.html @@ -0,0 +1,72 @@ +{% extends "django_nova/base.html" %} +{% block title %} - {{ project.projectname|capfirst }} Overview{% endblock %} +{% block pageclass %}overview{% endblock %} + +{% block headerjs %} +{{ block.super }} +<script type="text/javascript" src="/media/dashboard/js/django-admin.multiselect.js"></script> +<link rel="stylesheet" type="text/css" href="/media/dashboard/css/django-admin-widgets.css" /> + +<script type="text/javascript" charset="utf-8"> + $(function(){ + $.each($("#user_edit form select"), function () { + // "Locations" can be any label you want + SelectFilter.init(this.id, "Roles", 0, "/media/admin/"); + }); + }) +</script> +{% endblock %} + + +{% block content %} + <div id="page_head"> + <h2 id="page_heading">Edit User Roles</h2> + <p id="page_description">From here you can edit multiple user roles.</p> + </div> + + {% include "django_nova/_messages.html" %} + <div id="user_edit" class="dash_block first"> + {% if user %} + + <form action="." method="post" enctype="multipart/form-data"> + {% csrf_token %} + <fieldset class="module aligned {{ fieldset.classes }}"> + <h3 id="edit_{{ user.username }}">Edit Roles for User: {{ user.username }}</h3> + <div class="form-row"> + <label>User</label> + <span id="user_name">{{ user.username }}</span> + </div> + <input type="hidden" name="username" value="{{ user.id }}" id="username" /> + + {% for field in form.visible_fields %} + <div class="form-row"> + {{ field.errors }} + {{ field.label_tag }}{{ field }} + {% if field.field.help_text %}<p class="help">{{ field.field.help_text|safe }}</p>{% endif %} + </div> + {% endfor %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + </fieldset> + <div class="cancel"> + <a href="{% url nova_project_manage project.projectname %}">Cancel</a> + </div> + <div class="submit-row"> + <input type="submit" value="Save" class="default" /> + {# <a href="#" class="deletelink">Remove User</a> #} + </div> + </form> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No users are currently associated with this project. + </p> + </div> + </div> + {% endif %} + </div> +{% endblock %} + diff --git a/django-nova/src/django_nova/templates/django_nova/projects/index.html b/django-nova/src/django_nova/templates/django_nova/projects/index.html new file mode 100644 index 00000000..bd7b08c1 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/projects/index.html @@ -0,0 +1,26 @@ +{% extends "django_nova/base.html" %} +{% block title %} - {{ project.projectname|capfirst }} Overview{% endblock %} +{% block pageclass %}overview{% endblock %} + +{% block content %} + <div id="page_head"> + <h2><span>{{ project.projectname|capfirst }}</span> Overview</h2> + </div> + + {% include "django_nova/_messages.html" %} + + <div id="welcome"> + <p>Welcome to the <span>{{ project.projectname|capfirst }}</span> Overview. From here you can manage your instances, images, keys, and security groups.</p> + <p>To get started using the command line management tools, you can <a target="_blank" href="http://open.eucalyptus.com/wiki/Euca2oolsGuide_v1.1">download euca2ools</a> and use them with your x509 credentials.</p> + </div> + + <div id="resources" class="dash_block"> + <h3>Project Resources</h3> + <ul> + <li><a href="{% url nova_download_credentials project.projectname %}">Generate X509 credentials.</a></li> + <li><a href="{% url nova_instances project.projectname %}">View Instances (<strong>{{ instance_count }}</strong> running).</a></li> + <li><a href="{% url nova_images project.projectname %}">View Images.</a></li> + </ul> + </div> +{% endblock %} + diff --git a/django-nova/src/django_nova/templates/django_nova/projects/manage.html b/django-nova/src/django_nova/templates/django_nova/projects/manage.html new file mode 100644 index 00000000..d7dc4968 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/projects/manage.html @@ -0,0 +1,45 @@ +{% extends "django_nova/base.html" %} +{% block title %} - {{ project.projectname|capfirst }} Overview{% endblock %} +{% block pageclass %}overview{% endblock %} + +{% block content %} + <div id="page_head"> + <h2 id="page_heading">Manage Users and Roles</h2> + <p id="page_description">From here you can manage users and roles.</p> + </div> + + {% include "django_nova/_messages.html" %} + + <div id="users"> + {% if members %} + <table style="width: 100%"> + <tr> + <th>Username</th> + <th>Project Roles</th> + <th>Global Roles</th> + <th> </th> + </tr> + {% for member in members %} + <tr class="{% cycle 'odd' 'even' %}"> + <td>{{ member.memberId }} {% if project.projectManagerId == member.memberId %}(<em>project manager</em>){% endif %}</td> + <td>{{ member.project_roles }}</td> + <td>{{ member.global_roles }}</td> + <td class="odd"> + <a href="{% url nova_project_edit_user project.projectname member.memberId%}">Edit</a> + </td> + </tr> + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No users are currently associated with this project. + </p> + </div> + </div> + {% endif %} + </div> +{% endblock %} + diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/_authorize_form.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/_authorize_form.html new file mode 100644 index 00000000..80de4bee --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/_authorize_form.html @@ -0,0 +1,5 @@ +{% for field in authorize_form %} + {{ field.label_tag }} + {% if field.errors %}{{ field.errors }}{% endif %} + {{ field }} +{% endfor %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/_create_form.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/_create_form.html new file mode 100644 index 00000000..d295c406 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/_create_form.html @@ -0,0 +1,5 @@ +{% for field in create_form %} +{{ field.label_tag }} +{% if field.errors %}{{ field.errors }}{% endif %} +{{ field }} +{% endfor %} diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/_revoke_form.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/_revoke_form.html new file mode 100644 index 00000000..4a884cde --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/_revoke_form.html @@ -0,0 +1,3 @@ +<input type="hidden" name="protocol" value="{{ rule.ip_protocol }}" /> +<input type="hidden" name="from_port" value="{{ rule.from_port}}" /> +<input type="hidden" name="to_port" value="{{ rule.to_port }}" /> diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/base.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/base.html new file mode 100644 index 00000000..0e8a232e --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/base.html @@ -0,0 +1,7 @@ +{% extends "django_nova/base.html" %} +{% load sidebar_tags %} + +{% block nav_projects %} + {% sidebar_select securitygroups %} + {{ block.super }} +{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/detail.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/detail.html new file mode 100644 index 00000000..0e4afb9c --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/detail.html @@ -0,0 +1,62 @@ +{% extends "django_nova/securitygroups/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block content %} + <div id="dashboard_tabs"> + <div id="tabs-1" class="ui-tabs-panel ui-widget-content ui-corner-bottom dash-wrap" style="margin-left:0;min-height:300px;"> + <ul id="dashboard_nav"> + <li><a id="lnk_overview" href="{% url dashboard_project project.projectname %}">Overview</a></li> + <li><a id="lnk_instances" href="{% url dashboard_instances project.projectname %}">Instances</a></li> + <li><a id="lnk_images" href="{% url dashboard_images project.projectname %}">Images</a></li> + <li><a id="lnk_keypairs" href="{% url dashboard_keypairs project.projectname %}">Keys</a></li> + <li class="active"><a id="lnk_securitygroups" href="{% url dashboard_securitygroups project.projectname %}">Security Groups</a></li> + <li><a id="lnk_volumes" href="{% url dashboard_volumes project.projectname %}">Volumes</a></li> + </ul> + <div id="right_content"> + <div id="page_head"> + <h2>Security Group: {{ securitygroup.name }}</h2> + <p>Add and remove protocols to the security group by authorizing and revoking port forwarding. For instance<br /> [tcp, 80, 80] will allow access to HTTP from devices outside this security group.</p> + </div> + + {% include "django_nova/_messages.html" %} + + <table> + <tr> + <th>Protocol</th> + <th>From Port</th> + <th>To Port</th> + <th></th> + </tr> + {% for rule in securitygroup.rules %} + <tr class="{% cycle 'odd' 'even' %}"> + <td>{{ rule.ip_protocol }}</td> + <td class="odd">{{ rule.from_port }}</td> + <td>{{ rule.to_port }}</td> + <td class="odd"> + <form id="security_groups" method="post" action="{% url dashboard_securitygroups_revoke project.projectname securitygroup.name %}"> + {% csrf_token %} + {% include "django_nova/securitygroups/_revoke_form.html" %} + <input class="ui-state-default ui-corner-all" type="submit" value="Revoke" /> + </form> + </td> + </tr> + {% endfor %} + </table> + + <div class="block"> + <h3>Authorize</h3> + <form id="authorize" method="post" action="{% url dashboard_securitygroups_authorize project.projectname securitygroup.name %}"> + {% csrf_token %} + <fieldset> + <input type="hidden" name="group" value="{{ securitygroup.name }}" /> + {% include "django_nova/securitygroups/_authorize_form.html" %} + <input class="ui-state-default ui-corner-all" type="submit" value="Authorize"> + </fieldset> + </form> + </div> + </div> + <div class="clr"></div> + </div> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/securitygroups/index.html b/django-nova/src/django_nova/templates/django_nova/securitygroups/index.html new file mode 100644 index 00000000..a3c992c6 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/securitygroups/index.html @@ -0,0 +1,59 @@ +{% extends "django_nova/securitygroups/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block content %} + <div id="dashboard_tabs"> + <div id="tabs-1" class="ui-tabs-panel ui-widget-content ui-corner-bottom dash-wrap" style="margin-left:0px;min-height:300px;"> + <ul id="dashboard_nav"> + <li><a id="lnk_overview" href="{% url dashboard_project project.projectname %}">Overview</a></li> + <li><a id="lnk_instances" href="{% url dashboard_instances project.projectname %}">Instances</a></li> + <li><a id="lnk_images" href="{% url dashboard_images project.projectname %}">Images</a></li> + <li><a id="lnk_keypairs" href="{% url dashboard_keypairs project.projectname %}">Keys</a></li> + <li class="active"><a id="lnk_securitygroups" href="{% url dashboard_securitygroups project.projectname %}">Security Groups</a></li> + <li><a id="lnk_volumes" href="{% url dashboard_volumes project.projectname %}">Volumes</a></li> + </ul> + <div id="right_content"> + <div id="page_head"> + <h2 id="page_heading">Security Groups</h2> + <p id="page_description">Security groups are firewall rules which allow access to your instances from other groups as well as the internet. All ports/protocols are denied by default.</p> + </div> + + {% include "django_nova/_messages.html" %} + + <table style="width:100%;"> + <tr> + <th>Name</th> + <th style="min-width:60%;">Description</th> + <th>Rules</th> + <th> </th> + </tr> + {% for securitygroup in securitygroups %} + <tr class="{% cycle 'odd' 'even' %}"> + <td id="group_{{ securitygroup.id }}"><a href="{% url dashboard_securitygroups_detail project.projectname securitygroup.name %}">{{ securitygroup.name }}</a></td> + <td id="group_{{ securitygroup.id }}_description" class="odd">{{ securitygroup.description }}</td> + <td id="group_{{ securitygroup.id }}_rules">{{ securitygroup.rules|length }}</td> + <td class="odd"> + <form id="delete_group_{{ securitygroup.id }}" method="post" action="{% url dashboard_securitygroups_delete project.projectname securitygroup.name %}"> + {% csrf_token %} + <input class="ui-state-default ui-corner-all" type="submit" value="Delete"> + </form> + </td> + </tr> + {% endfor %} + </table> + <div class="block"> + <form id="add_group_form" method="post" action="{% url dashboard_securitygroups_add project.projectname %}"> + {% csrf_token %} + <fieldset> + <h3>New Group</h3> + {% include "django_nova/securitygroups/_create_form.html" %} + <label> </label><input class="ui-state-default ui-corner-all" type="submit" value="Create" /> + </fieldset> + </form> + </div> + </div> + <div class="clr"></div> + </div> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templates/django_nova/volumes/_attach_form.html b/django-nova/src/django_nova/templates/django_nova/volumes/_attach_form.html new file mode 100644 index 00000000..b473d74e --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/volumes/_attach_form.html @@ -0,0 +1,5 @@ +{% for field in attach_form %} +{{ field.label_tag }} +{% if field.errors %}{{ field.errors }}{% endif %} +{{ field }} +{% endfor %} diff --git a/django-nova/src/django_nova/templates/django_nova/volumes/_create_form.html b/django-nova/src/django_nova/templates/django_nova/volumes/_create_form.html new file mode 100644 index 00000000..d295c406 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/volumes/_create_form.html @@ -0,0 +1,5 @@ +{% for field in create_form %} +{{ field.label_tag }} +{% if field.errors %}{{ field.errors }}{% endif %} +{{ field }} +{% endfor %} diff --git a/django-nova/src/django_nova/templates/django_nova/volumes/base.html b/django-nova/src/django_nova/templates/django_nova/volumes/base.html new file mode 100644 index 00000000..88d1bced --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/volumes/base.html @@ -0,0 +1,7 @@ +{% extends "django_nova/base.html" %} +{% load sidebar_tags %} + +{% block nav_projects %} + {% sidebar_select volumes %} + {{ block.super }} +{% endblock %}
\ No newline at end of file diff --git a/django-nova/src/django_nova/templates/django_nova/volumes/index.html b/django-nova/src/django_nova/templates/django_nova/volumes/index.html new file mode 100644 index 00000000..2f03a066 --- /dev/null +++ b/django-nova/src/django_nova/templates/django_nova/volumes/index.html @@ -0,0 +1,84 @@ +{% extends "django_nova/volumes/base.html" %} + +{% block title %} - Cloud Computing{% endblock %} + +{% block content %} + <div id="page_head"> + <h2 id="page_heading">Volumes</h2> + <p id="page_description">Volumes provide persistent block storage. Creating a new volume gives you a raw block device which you may format with your choice of filesystems (ext3 is recommended). A volume may only be attached to a single instance at a time.</p> + </div> + + {% include "django_nova/_messages.html" %} + + {% if volumes %} + <table style="width: 100%"> + <tr> + <th>ID</th> + <th>Size</th> + <th colspan="2">Status</th> + </tr> + {% for volume in volumes %} + <tr class="{% cycle 'odd' 'even' %}"> + <td id="volume_{{ volume.id }}">{{ volume.id }} {{ volume.displayName }}</td> + <td id="volume_{{ volume.id }}_size" class="odd">{{ volume.size }}GB</td> + <td id="volume_{{ volume.id }})_status"> + {% if volume.status == "in-use" %} + {% if volume.attachment_state == "attached" %} + attached: {{ volume.attach_data.instance_id }} + {% else %} + {{ volume.attachment_state }} + {% endif %} + {% else %} + {{ volume.status }} + {% endif %} + </td> + {% if volume.attachment_state == "attached" %} + <td class="odd"> + <form class="volume" action="{% url nova_volumes_detach project.projectname volume.id %}" method="post"> + {% csrf_token %} + <input id="detach_{{ volume.id }}" class="detach" type="submit" value="Detach"> + </form> + </td> + {% else %} + <td class="odd"> + <form class="volume" action="{% url nova_volumes_delete project.projectname volume.id %}" method="post"> + {% csrf_token %} + <input id="delete_{{ volume.id }}" class="delete" type="submit" value="Delete"> + </form> + </td> + {% endif %} + </tr> + {% endfor %} + </table> + {% else %} + <div class="ui-widget"> + <div class="ui-state-highlight ui-corner-all"> + <p> + <span class="ui-icon ui-icon-info"></span> + No volumes currently exist. + </p> + </div> + </div> + {% endif %} + <div class="dash_block first"> + <form id="new_volume_form" method="post" action="{% url nova_volumes_add project.projectname %}"> + {% csrf_token %} + <fieldset> + <h3>Create New Volume</h3> + {% include "django_nova/volumes/_create_form.html" %} + <input id="create_volume" class="create" type="submit" value="Create" /> + </fieldset> + </form> + </div> + + <div class="dash_block"> + <form id="new_volume_form" method="post" action="{% url nova_volumes_attach project.projectname %}"> + {% csrf_token %} + <fieldset> + <h3>Attach Volume</h3> + {% include "django_nova/volumes/_attach_form.html" %} + <input id="attach_volume" class="attach" type="submit" value="Attach" /> + </fieldset> + </form> + </div> +{% endblock %} diff --git a/django-nova/src/django_nova/templatetags/__init__.py b/django-nova/src/django_nova/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/django-nova/src/django_nova/templatetags/__init__.py diff --git a/django-nova/src/django_nova/templatetags/admin_extras.py b/django-nova/src/django_nova/templatetags/admin_extras.py new file mode 100644 index 00000000..18e7792e --- /dev/null +++ b/django-nova/src/django_nova/templatetags/admin_extras.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Template tags for extending the Django admin interface. +""" + +from django.contrib.admin.templatetags.admin_list import items_for_result, result_headers +from django.core.urlresolvers import reverse +from django.template import Library +from django.utils.safestring import mark_safe + + +register = Library() + +def project_result_list(cl): + headers = list(result_headers(cl)) + headers.append({'text': mark_safe(' ')}) + + results = list() + + for project in cl.result_list: + rl = list(items_for_result(cl,project,None)) + + url = reverse('admin_project_sendcredentials', args=[project.projectname]) + content = mark_safe('<td><a href="%s">Send Credentials</a></td>' % url) + + rl.append(content) + results.append(rl) + + return { + 'cl': cl, + 'result_headers': headers, + 'results': results + } +project_result_list = register.inclusion_tag("admin/change_list_results.html")(project_result_list) diff --git a/django-nova/src/django_nova/templatetags/django_nova_tags.py b/django-nova/src/django_nova/templatetags/django_nova_tags.py new file mode 100644 index 00000000..f199af29 --- /dev/null +++ b/django-nova/src/django_nova/templatetags/django_nova_tags.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for working with django_nova. +""" + +from django import template +from django.conf import settings + + +register = template.Library() + + +class SiteBrandingNode(template.Node): + def render(self, context): + return settings.SITE_BRANDING + +@register.tag +def site_branding(parser, token): + return SiteBrandingNode() + diff --git a/django-nova/src/django_nova/templatetags/project_tags.py b/django-nova/src/django_nova/templatetags/project_tags.py new file mode 100644 index 00000000..9cdf2a67 --- /dev/null +++ b/django-nova/src/django_nova/templatetags/project_tags.py @@ -0,0 +1,39 @@ + # vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for gathering contextual region data. +""" + +from django import template +from django_nova.shortcuts import get_projects + + +register = template.Library() + + +class ProjectsNode(template.Node): + def render(self, context): + # Store project list in template context. + context['projects'] = get_projects(context['request'].user) + return '' + + +@register.tag +def load_projects(parser, token): + return ProjectsNode() diff --git a/django-nova/src/django_nova/templatetags/region_tags.py b/django-nova/src/django_nova/templatetags/region_tags.py new file mode 100644 index 00000000..9552229d --- /dev/null +++ b/django-nova/src/django_nova/templatetags/region_tags.py @@ -0,0 +1,40 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Template tags for gathering contextual region data. +""" + +from django import template +from django_nova.shortcuts import get_current_region, get_all_regions + + +register = template.Library() + + +class RegionsNode(template.Node): + def render(self, context): + # Store region info in template context. + context['current_region'] = get_current_region(context['request']) + context['regions'] = get_all_regions() + return '' + + +@register.tag +def load_regions(parser, token): + return RegionsNode() + diff --git a/django-nova/src/django_nova/templatetags/sidebar_tags.py b/django-nova/src/django_nova/templatetags/sidebar_tags.py new file mode 100644 index 00000000..e2175725 --- /dev/null +++ b/django-nova/src/django_nova/templatetags/sidebar_tags.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Template tags for rendering the sidebar. +""" + +from django import template + + +register = template.Library() + + +class SidebarSelectNode(template.Node): + def __init__(self, selected): + self.selected = selected + + def render(self, context): + # Store page type in template context. + context['sidebar_selected'] = self.selected + return '' + + +@register.tag +def sidebar_select(parser, token): + try: + tag_name, selected = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires exactly one argument" % token.contents.split()[0] + return SidebarSelectNode(str(selected)) + diff --git a/django-nova/src/django_nova/templatetags/truncate_filter.py b/django-nova/src/django_nova/templatetags/truncate_filter.py new file mode 100644 index 00000000..14098781 --- /dev/null +++ b/django-nova/src/django_nova/templatetags/truncate_filter.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Template tags for truncating strings. +""" + +from django import template + +register = template.Library() + +@register.filter("truncate") +def truncate(value, size): + if len(value) > size and size > 3: + return value[0:(size-3)] + '...' + else: + return value[0:size] diff --git a/django-nova/src/django_nova/tests/__init__.py b/django-nova/src/django_nova/tests/__init__.py new file mode 100644 index 00000000..7470671d --- /dev/null +++ b/django-nova/src/django_nova/tests/__init__.py @@ -0,0 +1 @@ +from view_tests import *
\ No newline at end of file diff --git a/django-nova/src/django_nova/tests/templates/base-sidebar.html b/django-nova/src/django_nova/tests/templates/base-sidebar.html new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/django-nova/src/django_nova/tests/templates/base-sidebar.html diff --git a/django-nova/src/django_nova/tests/urls.py b/django-nova/src/django_nova/tests/urls.py new file mode 100644 index 00000000..d85381c2 --- /dev/null +++ b/django-nova/src/django_nova/tests/urls.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +URL patterns for testing django-nova views. +""" + +from django.conf.urls.defaults import * +from django.conf.urls.defaults import * + + +urlpatterns = patterns('', + url(r'^projects/', include('django_nova.urls.project')), + url(r'^region/', include('django_nova.urls.region')), + url(r'^admin/projects/', include('django_nova.urls.admin_project')), + url(r'^admin/roles/', include('django_nova.urls.admin_roles')), + url(r'^credentials/download/(?P<auth_token>\w+)/$', + 'django_nova.views.credentials.authorize_credentials', + name='nova_credentials_authorize'), +) + diff --git a/django-nova/src/django_nova/tests/view_tests/__init__.py b/django-nova/src/django_nova/tests/view_tests/__init__.py new file mode 100644 index 00000000..51d01c59 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/__init__.py @@ -0,0 +1,7 @@ +from credential_tests import * +from image_tests import * +from instance_tests import * +from keypair_tests import * +from region_tests import * +from volume_tests import * + diff --git a/django-nova/src/django_nova/tests/view_tests/base.py b/django-nova/src/django_nova/tests/view_tests/base.py new file mode 100644 index 00000000..8b394d0e --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/base.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base classes for view based unit tests. +""" + +import mox + +from django import test +from django.conf import settings +from django.contrib.auth import models as auth_models +from django_nova import adminclient +from django_nova import manager +from django_nova import shortcuts + + +TEST_PROJECT = 'test' +TEST_USER = 'test' +TEST_REGION = 'test' + + +class BaseViewTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + + def tearDown(self): + self.mox.UnsetStubs() + + def assertRedirectsNoFollow(self, response, expected_url): + self.assertEqual(response._headers['location'], + ('Location', settings.TESTSERVER + expected_url)) + self.assertEqual(response.status_code, 302) + + def authenticateTestUser(self): + user = auth_models.User.objects.create_user(TEST_USER, + 'test@test.com', + password='test') + login = self.client.login(username=TEST_USER, password='test') + self.failUnless(login, 'Unable to login') + return user + + +class BaseProjectViewTests(BaseViewTests): + def setUp(self): + super(BaseProjectViewTests, self).setUp() + + project = adminclient.ProjectInfo() + project.projectname = TEST_PROJECT + project.projectManagerId = TEST_USER + + self.user = self.authenticateTestUser() + self.region = adminclient.RegionInfo(name=TEST_REGION, + endpoint='http://test:8773/') + self.project = manager.ProjectManager(self.user.username, + project, + self.region) + self.mox.StubOutWithMock(shortcuts, 'get_project_or_404') + shortcuts.get_project_or_404(mox.IgnoreArg(), + 'test').AndReturn(self.project) + + def create_key_pair_choices(self, key_names): + return [(k, k) for k in key_names] + + def create_instance_type_choices(self): + return [('m1.medium', 'm1.medium'), + ('m1.large', 'm1.large')] + + def create_instance_choices(self, instance_ids): + return [(id, id) for id in instance_ids] + + def create_available_volume_choices(self, volumes): + return [(v.id, '%s %s - %dGB' % (v.id, v.displayName, v.size)) \ + for v in volumes] + diff --git a/django-nova/src/django_nova/tests/view_tests/credential_tests.py b/django-nova/src/django_nova/tests/view_tests/credential_tests.py new file mode 100644 index 00000000..5838ba91 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/credential_tests.py @@ -0,0 +1,70 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for credential views. +""" + +import mox +from django.conf import settings +from django.core.urlresolvers import reverse +from django_nova import models +from django_nova.tests.view_tests.base import BaseViewTests + + +class CredentialViewTests(BaseViewTests): + def test_download_expired_credentials(self): + auth_token = 'expired' + self.mox.StubOutWithMock(models.CredentialsAuthorization, + 'get_by_token') + models.CredentialsAuthorization.get_by_token(auth_token) \ + .AndReturn(None) + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_credentials_authorize', + args=[auth_token])) + self.assertTemplateUsed(res, 'django_nova/credentials/expired.html') + + self.mox.VerifyAll() + + def test_download_good_credentials(self): + auth_token = 'good' + + creds = models.CredentialsAuthorization() + creds.username = 'test' + creds.project = 'test' + creds.auth_token = auth_token + + self.mox.StubOutWithMock(models.CredentialsAuthorization, + 'get_by_token') + self.mox.StubOutWithMock(creds, 'get_zip') + models.CredentialsAuthorization.get_by_token(auth_token) \ + .AndReturn(creds) + creds.get_zip().AndReturn('zip') + + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_credentials_authorize', + args=[auth_token])) + self.assertEqual(res.status_code, 200) + self.assertEqual(res['Content-Disposition'], + 'attachment; filename=%s-test-test-x509.zip' % + settings.SITE_NAME) + self.assertContains(res, 'zip') + + self.mox.VerifyAll() diff --git a/django-nova/src/django_nova/tests/view_tests/image_tests.py b/django-nova/src/django_nova/tests/view_tests/image_tests.py new file mode 100644 index 00000000..7e1ee838 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/image_tests.py @@ -0,0 +1,232 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for image views. +""" + +import boto.ec2.image +import boto.ec2.instance +import mox + +from django.core.urlresolvers import reverse +from django_nova import forms +from django_nova import shortcuts +from django_nova.tests.view_tests.base import BaseProjectViewTests, TEST_PROJECT + + +TEST_IMAGE_ID = 'ami_test' +TEST_INSTANCE_ID = 'i-abcdefg' +TEST_KEY = 'foo' + + +class ImageViewTests(BaseProjectViewTests): + def setUp(self): + self.ami = boto.ec2.image.Image() + self.ami.id = TEST_IMAGE_ID + setattr(self.ami, 'displayName', TEST_IMAGE_ID) + setattr(self.ami, 'description', TEST_IMAGE_ID) + super(ImageViewTests, self).setUp() + + def test_index(self): + self.mox.StubOutWithMock(self.project, 'get_images') + self.mox.StubOutWithMock(forms, 'get_key_pair_choices') + self.mox.StubOutWithMock(forms, 'get_instance_type_choices') + + self.project.get_images().AndReturn([]) + forms.get_key_pair_choices(self.project).AndReturn([]) + forms.get_instance_type_choices().AndReturn([]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_images', args=[TEST_PROJECT])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/images/index.html') + self.assertEqual(len(res.context['image_lists']), 3) + + self.mox.VerifyAll() + + def test_launch_form(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.mox.StubOutWithMock(forms, 'get_key_pair_choices') + self.mox.StubOutWithMock(forms, 'get_instance_type_choices') + + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + forms.get_key_pair_choices(self.project).AndReturn([]) + forms.get_instance_type_choices().AndReturn([]) + + self.mox.ReplayAll() + + args = [TEST_PROJECT, TEST_IMAGE_ID] + res = self.client.get(reverse('nova_images_launch', args=args)) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/images/launch.html') + self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) + + self.mox.VerifyAll() + + def test_launch(self): + instance = boto.ec2.instance.Instance() + instance.id = TEST_INSTANCE_ID + instance.image_id = TEST_IMAGE_ID + reservation = boto.ec2.instance.Reservation() + reservation.instances = [instance] + + self.mox.StubOutWithMock(forms, 'get_key_pair_choices') + self.mox.StubOutWithMock(forms, 'get_instance_type_choices') + self.mox.StubOutWithMock(self.project, 'run_instances') + + forms.get_key_pair_choices(self.project).AndReturn( + self.create_key_pair_choices([TEST_KEY])) + forms.get_instance_type_choices().AndReturn( + self.create_instance_type_choices()) + self.project.run_instances(TEST_IMAGE_ID, + addressing_type=mox.IgnoreArg(), + key_name=TEST_KEY, + user_data='', + instance_type='m1.medium', + min_count='1', + max_count='1').AndReturn(reservation) + self.mox.ReplayAll() + + url = reverse('nova_images_launch', args=[TEST_PROJECT, TEST_IMAGE_ID]) + data = {'key_name': TEST_KEY, + 'count': '1', + 'size': 'm1.medium', + 'user_data': ''} + res = self.client.post(url, data) + self.assertRedirectsNoFollow(res, reverse('nova_instances', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_detail(self): + self.mox.StubOutWithMock(self.project, 'get_images') + self.mox.StubOutWithMock(self.project, 'get_image') + self.mox.StubOutWithMock(shortcuts, 'get_user_image_permissions') + self.mox.StubOutWithMock(forms, 'get_key_pair_choices') + self.mox.StubOutWithMock(forms, 'get_instance_type_choices') + + self.project.get_images().AndReturn([self.ami]) + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + shortcuts.get_user_image_permissions(mox.IgnoreArg(), + TEST_PROJECT).AndReturn(True) + forms.get_key_pair_choices(self.project).AndReturn( + self.create_key_pair_choices([TEST_KEY])) + forms.get_instance_type_choices().AndReturn( + self.create_instance_type_choices()) + + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_images_detail', + args=[TEST_PROJECT, TEST_IMAGE_ID])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/images/index.html') + self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) + + self.mox.VerifyAll() + + def test_remove_form(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_images_remove', + args=[TEST_PROJECT, TEST_IMAGE_ID])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/images/detail_list.html') + self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) + + self.mox.VerifyAll() + + def test_remove(self): + self.mox.StubOutWithMock(self.project, 'deregister_image') + self.project.deregister_image(TEST_IMAGE_ID).AndReturn(True) + self.mox.ReplayAll() + + res = self.client.post(reverse('nova_images_remove', + args=[TEST_PROJECT, TEST_IMAGE_ID])) + self.assertRedirectsNoFollow(res, reverse('nova_images', + args=[TEST_PROJECT])) + + self.mox.VerifyAll() + + def test_make_public(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.mox.StubOutWithMock(self.project, 'modify_image_attribute') + + self.ami.is_public = False + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + self.project.modify_image_attribute(TEST_IMAGE_ID, + attribute='launchPermission', + operation='add').AndReturn(True) + self.mox.ReplayAll() + + res = self.client.post(reverse('nova_images_privacy', + args=[TEST_PROJECT, TEST_IMAGE_ID])) + self.assertRedirectsNoFollow(res, reverse('nova_images_detail', + args=[TEST_PROJECT, TEST_IMAGE_ID])) + self.mox.VerifyAll() + + def test_make_private(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.mox.StubOutWithMock(self.project, 'modify_image_attribute') + + self.ami.is_public = True + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + self.project.modify_image_attribute(TEST_IMAGE_ID, + attribute='launchPermission', + operation='remove').AndReturn(True) + self.mox.ReplayAll() + + args = [TEST_PROJECT, TEST_IMAGE_ID] + res = self.client.post(reverse('nova_images_privacy', args=args)) + self.assertRedirectsNoFollow(res, reverse('nova_images_detail', + args=args)) + self.mox.VerifyAll() + + def test_update_form(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + self.mox.ReplayAll() + + args = [TEST_PROJECT, TEST_IMAGE_ID] + res = self.client.get(reverse('nova_images_update', args=args)) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/images/edit.html') + self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID) + + self.mox.VerifyAll() + + def test_update(self): + self.mox.StubOutWithMock(self.project, 'get_image') + self.mox.StubOutWithMock(self.project, 'update_image') + + self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami) + self.project.update_image(TEST_IMAGE_ID, 'test', 'test').AndReturn(True) + + self.mox.ReplayAll() + + args = [TEST_PROJECT, TEST_IMAGE_ID] + data = {'nickname': 'test', + 'description': 'test'} + url = reverse('nova_images_update', args=args) + res = self.client.post(url, data) + expected_url = reverse('nova_images_detail', args=args) + self.assertRedirectsNoFollow(res, expected_url) + + self.mox.VerifyAll() diff --git a/django-nova/src/django_nova/tests/view_tests/instance_tests.py b/django-nova/src/django_nova/tests/view_tests/instance_tests.py new file mode 100644 index 00000000..890b0640 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/instance_tests.py @@ -0,0 +1,67 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for instance views. +""" + +import boto.ec2.instance +import mox + +from django.core.urlresolvers import reverse +from django_nova.tests.view_tests.base import BaseProjectViewTests, TEST_PROJECT + + +TEST_INSTANCE_ID = 'i-abcdefgh' + + +class InstanceViewTests(BaseProjectViewTests): + def test_index(self): + self.mox.StubOutWithMock(self.project, 'get_instances') + self.project.get_instances().AndReturn([]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_instances', args=[TEST_PROJECT])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/instances/index.html') + self.assertEqual(len(res.context['instances']), 0) + + self.mox.VerifyAll() + + def test_detail(self): + instance = boto.ec2.instance.Instance() + instance.id = TEST_INSTANCE_ID + instance.displayName = instance.id + instance.displayDescription = instance.id + + self.mox.StubOutWithMock(self.project, 'get_instance') + self.project.get_instance(instance.id).AndReturn(instance) + self.mox.StubOutWithMock(self.project, 'get_instances') + self.project.get_instances().AndReturn([instance]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_instances_detail', + args=[TEST_PROJECT, TEST_INSTANCE_ID])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'django_nova/instances/index.html') + self.assertEqual(res.context['selected_instance'].id, instance.id) + + self.mox.VerifyAll() + diff --git a/django-nova/src/django_nova/tests/view_tests/keypair_tests.py b/django-nova/src/django_nova/tests/view_tests/keypair_tests.py new file mode 100644 index 00000000..95085ce3 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/keypair_tests.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for key pair views. +""" + +import boto.ec2.keypair +import mox + +from django.core.urlresolvers import reverse +from django_nova.tests.view_tests.base import (BaseProjectViewTests, + TEST_PROJECT) + + +TEST_KEY = 'test_key' + + +class KeyPairViewTests(BaseProjectViewTests): + def test_index(self): + self.mox.StubOutWithMock(self.project, 'get_key_pairs') + self.project.get_key_pairs().AndReturn([]) + + self.mox.ReplayAll() + + response = self.client.get(reverse('nova_keypairs', + args=[TEST_PROJECT])) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_nova/keypairs/index.html') + self.assertEqual(len(response.context['keypairs']), 0) + + self.mox.VerifyAll() + + def test_add_keypair(self): + key = boto.ec2.keypair.KeyPair() + key.name = TEST_KEY + + self.mox.StubOutWithMock(self.project, 'create_key_pair') + self.project.create_key_pair(key.name).AndReturn(key) + self.mox.StubOutWithMock(self.project, 'has_key_pair') + self.project.has_key_pair(key.name).AndReturn(False) + + self.mox.ReplayAll() + + url = reverse('nova_keypairs_add', args=[TEST_PROJECT]) + data = {'js': '0', 'name': key.name} + res = self.client.post(url, data) + self.assertEqual(res.status_code, 200) + self.assertEqual(res['Content-Type'], 'application/binary') + + self.mox.VerifyAll() + + def test_delete_keypair(self): + self.mox.StubOutWithMock(self.project, 'delete_key_pair') + self.project.delete_key_pair(TEST_KEY).AndReturn(None) + + self.mox.ReplayAll() + + data = {'key_name': TEST_KEY} + url = reverse('nova_keypairs_delete', args=[TEST_PROJECT]) + res = self.client.post(url, data) + self.assertRedirectsNoFollow(res, reverse('nova_keypairs', + args=[TEST_PROJECT])) + + self.mox.VerifyAll() + + def test_download_keypair(self): + material = 'abcdefgh' + session = self.client.session + session['key.%s' % TEST_KEY] = material + session.save() + + res = self.client.get(reverse('nova_keypairs_download', + args=['test', TEST_KEY])) + self.assertEqual(res.status_code, 200) + self.assertEqual(res['Content-Type'], 'application/binary') + self.assertContains(res, material) + diff --git a/django-nova/src/django_nova/tests/view_tests/region_tests.py b/django-nova/src/django_nova/tests/view_tests/region_tests.py new file mode 100644 index 00000000..6e4171ff --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/region_tests.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for region views. +""" + +from django.core.urlresolvers import reverse +from django_nova.tests.view_tests.base import BaseViewTests +from django_nova import shortcuts + + +TEST_REGION = 'one' + + +class RegionViewTests(BaseViewTests): + def test_change(self): + self.authenticateTestUser() + session = self.client.session + session['region'] = 'two' + session.save() + + data = {'redirect_url': '/', + 'region': TEST_REGION} + res = self.client.post(reverse('region_change'), data) + self.assertEqual(self.client.session['region'], TEST_REGION) + self.assertRedirectsNoFollow(res, '/') + diff --git a/django-nova/src/django_nova/tests/view_tests/volume_tests.py b/django-nova/src/django_nova/tests/view_tests/volume_tests.py new file mode 100644 index 00000000..cd917406 --- /dev/null +++ b/django-nova/src/django_nova/tests/view_tests/volume_tests.py @@ -0,0 +1,170 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Unit tests for volume views. +""" + +import boto.ec2.volume +import mox + +from django.core.urlresolvers import reverse +from django_nova import forms +from django_nova.tests.view_tests.base import (BaseProjectViewTests, + TEST_PROJECT) + + +TEST_VOLUME = 'vol-0000001' + + +class VolumeTests(BaseProjectViewTests): + def test_index(self): + instance_id = 'i-abcdefgh' + + volume = boto.ec2.volume.Volume() + volume.id = TEST_VOLUME + volume.displayName = TEST_VOLUME + volume.size = 1 + + self.mox.StubOutWithMock(self.project, 'get_volumes') + self.mox.StubOutWithMock(forms, 'get_available_volume_choices') + self.mox.StubOutWithMock(forms, 'get_instance_choices') + self.project.get_volumes().AndReturn([]) + forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn( + self.create_available_volume_choices([volume])) + forms.get_instance_choices(mox.IgnoreArg()).AndReturn( + self.create_instance_choices([instance_id])) + + self.mox.ReplayAll() + + response = self.client.get(reverse('nova_volumes', + args=[TEST_PROJECT])) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'django_nova/volumes/index.html') + self.assertEqual(len(response.context['volumes']), 0) + + self.mox.VerifyAll() + + def test_add_get(self): + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_volumes_add', args=[TEST_PROJECT])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_add_post(self): + vol = boto.ec2.volume.Volume() + vol.name = TEST_VOLUME + vol.displayName = TEST_VOLUME + vol.size = 1 + + self.mox.StubOutWithMock(self.project, 'create_volume') + self.project.create_volume(vol.size, vol.name, vol.name).AndReturn(vol) + + self.mox.ReplayAll() + + url = reverse('nova_volumes_add', args=[TEST_PROJECT]) + data = {'size': '1', + 'nickname': TEST_VOLUME, + 'description': TEST_VOLUME} + res = self.client.post(url, data) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_delete_get(self): + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_volumes_delete', + args=[TEST_PROJECT, TEST_VOLUME])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_delete_post(self): + self.mox.StubOutWithMock(self.project, 'delete_volume') + self.project.delete_volume(TEST_VOLUME).AndReturn(True) + + self.mox.ReplayAll() + + res = self.client.post(reverse('nova_volumes_delete', + args=[TEST_PROJECT, TEST_VOLUME])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_attach_get(self): + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_volumes_attach', + args=[TEST_PROJECT])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_attach_post(self): + volume = boto.ec2.volume.Volume() + volume.id = TEST_VOLUME + volume.displayName = TEST_VOLUME + volume.size = 1 + + instance_id = 'i-abcdefgh' + device = '/dev/vdb' + + self.mox.StubOutWithMock(self.project, 'attach_volume') + self.mox.StubOutWithMock(forms, 'get_available_volume_choices') + self.mox.StubOutWithMock(forms, 'get_instance_choices') + self.project.attach_volume(TEST_VOLUME, instance_id, device) \ + .AndReturn(True) + forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn( + self.create_available_volume_choices([volume])) + forms.get_instance_choices(mox.IgnoreArg()).AndReturn( + self.create_instance_choices([instance_id])) + + self.mox.ReplayAll() + + url = reverse('nova_volumes_attach', args=[TEST_PROJECT]) + data = {'volume': TEST_VOLUME, + 'instance': instance_id, + 'device': device} + res = self.client.post(url, data) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_detach_get(self): + self.mox.ReplayAll() + + res = self.client.get(reverse('nova_volumes_detach', + args=[TEST_PROJECT, TEST_VOLUME])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() + + def test_detach_post(self): + self.mox.StubOutWithMock(self.project, 'detach_volume') + self.project.detach_volume(TEST_VOLUME).AndReturn(True) + + self.mox.ReplayAll() + + res = self.client.post(reverse('nova_volumes_detach', + args=[TEST_PROJECT, TEST_VOLUME])) + self.assertRedirectsNoFollow(res, reverse('nova_volumes', + args=[TEST_PROJECT])) + self.mox.VerifyAll() diff --git a/django-nova/src/django_nova/testsettings.py b/django-nova/src/django_nova/testsettings.py new file mode 100644 index 00000000..80eb224f --- /dev/null +++ b/django-nova/src/django_nova/testsettings.py @@ -0,0 +1,21 @@ +import os + +ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) +DEBUG = True +TESTSERVER = 'http://testserver' +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = '/tmp/django-nova.db' +INSTALLED_APPS = ['django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django_nova'] +ROOT_URLCONF = 'django_nova.tests.urls' +TEMPLATE_DIRS = ( + os.path.join(ROOT_PATH, 'tests', 'templates') +) +SITE_BRANDING = 'OpenStack' +SITE_NAME = 'openstack' +NOVA_DEFAULT_ENDPOINT = 'none' +NOVA_DEFAULT_REGION = 'test' +NOVA_ACCESS_KEY = 'test' +NOVA_SECRET_KEY = 'test' diff --git a/django-nova/src/django_nova/urls/__init__.py b/django-nova/src/django_nova/urls/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/django-nova/src/django_nova/urls/__init__.py diff --git a/django-nova/src/django_nova/urls/admin_project.py b/django-nova/src/django_nova/urls/admin_project.py new file mode 100644 index 00000000..c32e2adb --- /dev/null +++ b/django-nova/src/django_nova/urls/admin_project.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +URL patterns for managing Nova projects through the Django admin interface. +""" + +from django.conf.urls.defaults import * + + +#TODO(devcamcar): Standardize url names admin_project_*. + +urlpatterns = patterns('', + url(r'^$', + 'django_nova.views.admin.projects_list', + name='admin_projects'), + url(r'^add/$', + 'django_nova.views.admin.add_project', + name='add_project'), + url(r'^(?P<project_name>[^/]+)/$', + 'django_nova.views.admin.project_view', + name='admin_project'), + url(r'^(?P<project_name>[^/]+)/user/(?P<project_user>[^/]+)/delete/', + 'django_nova.views.admin.delete_project_user', + name='admin_project_delete_user'), + url(r'^(?P<project_name>[^/]+)/delete/$', + 'django_nova.views.admin.delete_project', + name='delete_project'), + url(r'^(?P<project_name>[^/]+)/user/add/$', + 'django_nova.views.admin.add_project_user', + name='add_project_user'), + url(r'^(?P<project_name>[^/]+)/user/(?P<project_user>[^/]+)/$', + 'django_nova.views.admin.project_user', + name='project_user'), + url(r'^(?P<project_id>[^/]+)/sendcredentials/$', + 'django_nova.views.admin.project_sendcredentials', + name='admin_project_sendcredentials'), + url(r'^(?P<project_id>[^/]+)/start_vpn/$', + 'django_nova.views.admin.project_start_vpn', + name='admin_project_start_vpn'), +) diff --git a/django-nova/src/django_nova/urls/admin_roles.py b/django-nova/src/django_nova/urls/admin_roles.py new file mode 100644 index 00000000..d23380bc --- /dev/null +++ b/django-nova/src/django_nova/urls/admin_roles.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +URL patterns for managing Nova user roles through the Django admin interface. +""" + +from django.conf.urls.defaults import * + + +urlpatterns = patterns('', + url(r'^(?P<user_id>[^/]+)/$', + 'django_nova.views.admin.user_roles', + name='admin_user_roles'), + url(r'^$', + 'django_nova.views.admin.users_list', + name='admin_users_list'), +) diff --git a/django-nova/src/django_nova/urls/project.py b/django-nova/src/django_nova/urls/project.py new file mode 100644 index 00000000..a5cc6056 --- /dev/null +++ b/django-nova/src/django_nova/urls/project.py @@ -0,0 +1,129 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +URL patterns for managing Nova projects. +""" + +from django.conf.urls.defaults import * + + +urlpatterns = patterns('', + url(r'^(?P<project_id>[^/]+)/$', + 'django_nova.views.projects.detail', + name='nova_project'), + url(r'^(?P<project_id>[^/]+)/manage/(?P<username>[^/]+)/', + 'django_nova.views.projects.edit_user', + name='nova_project_edit_user'), + url(r'^(?P<project_id>[^/]+)/manage$', + 'django_nova.views.projects.manage', + name='nova_project_manage'), + url(r'^(?P<project_id>[^/]+)/download/credentials$', + 'django_nova.views.projects.download_credentials', + name='nova_download_credentials'), + url(r'^(?P<project_id>[^/]+)/images$', + 'django_nova.views.images.index', + name='nova_images'), + url(r'^(?P<project_id>[^/]+)/images/(?P<image_id>[^/]+)/launch$', + 'django_nova.views.images.launch', + name='nova_images_launch'), + url(r'^(?P<project_id>[^/]+)/images/(?P<image_id>[^/]+)/remove$', + 'django_nova.views.images.remove', + name='nova_images_remove'), + url(r'^(?P<project_id>[^/]+)/images/(?P<image_id>[^/]+)/update$', + 'django_nova.views.images.update', + name='nova_images_update'), + url(r'^(?P<project_id>[^/]+)/images/(?P<image_id>[^/]+)/detail$', + 'django_nova.views.images.detail', + name='nova_images_detail'), + url(r'^(?P<project_id>[^/]+)/images/(?P<image_id>[^/]+)$', + 'django_nova.views.images.privacy', + name='nova_images_privacy'), + url(r'^(?P<project_id>[^/]+)/instances$', + 'django_nova.views.instances.index', + name='nova_instances'), + url(r'^(?P<project_id>[^/]+)/instances/refresh$', + 'django_nova.views.instances.refresh', + name='nova_instances_refresh'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>[^/]+)/refresh$', + 'django_nova.views.instances.refresh_detail', + name='nova_instances_refresh_detail'), + url(r'^(?P<project_id>[^/]+)/instances/terminate$', + 'django_nova.views.instances.terminate', + name='nova_instances_terminate'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>[^/]+)$', + 'django_nova.views.instances.detail', + name='nova_instances_detail'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>[^/]+)/performance$', + 'django_nova.views.instances.performance', + name='nova_instances_performance'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>[^/]+)/console$', + 'django_nova.views.instances.console', + name='nova_instances_console'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>.*)/update$', + 'django_nova.views.instances.update', + name='nova_instance_update'), + url(r'^(?P<project_id>[^/]+)/instances/(?P<instance_id>[^/]+)/graph/(?P<graph_name>[^/]+)$', + 'django_nova.views.instances.graph', + name='nova_instances_graph'), + url(r'^(?P<project_id>[^/]+)/keys$', + 'django_nova.views.keypairs.index', + name='nova_keypairs'), + url(r'^(?P<project_id>[^/]+)/keys/add$', + 'django_nova.views.keypairs.add', + name='nova_keypairs_add'), + url(r'^(?P<project_id>[^/]+)/keys/delete$', + 'django_nova.views.keypairs.delete', + name='nova_keypairs_delete'), + url(r'^(?P<project_id>[^/]+)/keys/(?P<key_name>.*)/download$', + 'django_nova.views.keypairs.download', + name='nova_keypairs_download'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/$', + # 'django_nova.views.securitygroups.index', + # name='nova_securitygroups'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/add$', + # 'django_nova.views.securitygroups.add', + # name='nova_securitygroups_add'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/(?P<group_name>[^/]+)$', + # 'django_nova.views.securitygroups.detail', + # name='nova_securitygroups_detail'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/(?P<group_name>[^/]+)/authorize/$', + # 'django_nova.views.securitygroups.authorize', + # name='nova_securitygroups_authorize'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/(?P<group_name>[^/]+)/delete/$', + # 'django_nova.views.securitygroups.delete', + # name='nova_securitygroups_delete'), + #url(r'^(?P<project_id>[^/]+)/securitygroups/(?P<group_name>.*)/revoke/$', + # 'django_nova.views.securitygroups.revoke', + # name='nova_securitygroups_revoke'), + url(r'^(?P<project_id>[^/]+)/volumes/$', + 'django_nova.views.volumes.index', + name='nova_volumes'), + url(r'^(?P<project_id>[^/]+)/volumes/add$', + 'django_nova.views.volumes.add', + name='nova_volumes_add'), + url(r'^(?P<project_id>[^/]+)/volumes/attach$', + 'django_nova.views.volumes.attach', + name='nova_volumes_attach'), + url(r'^(?P<project_id>[^/]+)/volumes/(?P<volume_id>[^/]+)/detach$', + 'django_nova.views.volumes.detach', + name='nova_volumes_detach'), + url(r'^(?P<project_id>[^/]+)/volumes/(?P<volume_id>[^/]+)/delete$', + 'django_nova.views.volumes.delete', + name='nova_volumes_delete'), +) diff --git a/django-nova/src/django_nova/urls/region.py b/django-nova/src/django_nova/urls/region.py new file mode 100644 index 00000000..dde1e01b --- /dev/null +++ b/django-nova/src/django_nova/urls/region.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +URL patterns for managing Nova regions. +""" + +from django.conf.urls.defaults import * + + +urlpatterns = patterns('', + url(r'^change/$', + 'django_nova.views.regions.change', + name='region_change'), +) diff --git a/django-nova/src/django_nova/views/__init__.py b/django-nova/src/django_nova/views/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/django-nova/src/django_nova/views/__init__.py diff --git a/django-nova/src/django_nova/views/admin.py b/django-nova/src/django_nova/views/admin.py new file mode 100644 index 00000000..ec1b4390 --- /dev/null +++ b/django-nova/src/django_nova/views/admin.py @@ -0,0 +1,326 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova through the Django admin interface. +""" + +import boto.exception +from django import http +from django import template +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.contrib.auth import models as auth_models +from django.shortcuts import redirect, render_to_response +from django_nova import forms +from django_nova import models +from django_nova.connection import get_nova_admin_connection + + +@staff_member_required +def project_sendcredentials(request, project_id): + nova = get_nova_admin_connection() + project = nova.get_project(project_id) + + users = [user.memberId for user in nova.get_project_members(project_id)] + form = forms.SendCredentialsForm(query_list=users) + + if project == None: + raise http.Http404() + + if request.method == 'POST': + if len(request.POST.getlist('users')) < 1: + msg = "Please select a user to send credentials to." + + return render_to_response('admin/django_nova/project/send_credentials.html', { + 'project' : project, + 'form' : form, + 'users' : users, + 'error': msg, + }, context_instance = template.RequestContext(request)) + else: + for username in request.POST.getlist('users'): + models.CredentialsAuthorization.authorize(username, project_id) + msg = "Credentials were successfully sent." + return render_to_response('admin/django_nova/project/send_credentials.html', { + 'project' : project, + 'form' : form, + 'users' : users, + 'success': msg, + }, context_instance = template.RequestContext(request)) + + return render_to_response('admin/django_nova/project/send_credentials.html', { + 'project' : project, + 'form' : form, + 'users' : users, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def project_start_vpn(request, project_id): + nova = get_nova_admin_connection() + project = nova.get_project(project_id) + + if project == None: + raise http.Http404() + + try: + nova.start_vpn(project_id) + messages.success(request, + 'Successfully started VPN for project %s.' % + project_id) + except boto.exception.EC2ResponseError, e: + messages.error(request, + 'Unable to start VPN for the project %s: %s - %s' % + (project_id, e.code, e.error_message)) + + return redirect('admin_projects') + + +@staff_member_required +def projects_list(request): + nova = get_nova_admin_connection() + projects = nova.get_projects() + + return render_to_response('admin/django_nova/project/project_list.html', { + 'projects' : projects + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def project_view(request, project_name): + nova = get_nova_admin_connection() + project = nova.get_project(project_name) + users = nova.get_project_members(project_name) + try: + manager = auth_models.User.objects.get(username=project.projectManagerId) + except auth_models.User.DoesNotExist: + manager = None + + for user in users: + project_role = [str(role.role) for role in nova.get_user_roles(user.memberId, project_name)] + global_role = [str(role.role) for role in nova.get_user_roles(user.memberId, project=False)] + + user.project_roles = ", ".join(project_role) + user.global_roles = ", ".join(global_role) + + return render_to_response('admin/django_nova/project/edit_project.html', { + 'project' : project, + 'users' : users, + 'projectname': project.projectname, + 'manager': manager, + 'description': project.description, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def add_project(request): + nova = get_nova_admin_connection() + + if request.method == 'POST': + form = forms.ProjectForm(request.POST) + if form.is_valid(): + manager = form.cleaned_data["manager"] + nova.create_project(form.cleaned_data["projectname"], + manager.username, + form.cleaned_data["description"]) + return redirect('admin_project', request.POST["projectname"]) + else: + form = forms.ProjectForm() + + return render_to_response('admin/django_nova/project/add_project.html', { + 'form' : form, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def delete_project(request, project_name): + nova = get_nova_admin_connection() + + if request.method == 'POST': + nova.delete_project(project_name) + return redirect('admin_projects') + + project = nova.get_project(project_name) + + return render_to_response('admin/django_nova/project/delete_project.html', { + 'project' : project, + }, context_instance = template.RequestContext(request)) + +def remove_project_roles(username, project): + nova = get_nova_admin_connection() + userroles = nova.get_user_roles(username, project) + roles = [str(role.role) for role in userroles] + + for role in roles: + if role == "developer": + nova.remove_user_role(username, "developer", project) + if role == "sysadmin": + nova.remove_user_role(username, "sysadmin", project) + if role == "netadmin": + nova.remove_user_role(username, "netadmin", project) + +def remove_global_roles(username): + nova = get_nova_admin_connection() + userroles = nova.get_user_roles(username) + roles = [str(role.role) for role in userroles] + + for role in roles: + if role == "developer": + nova.remove_user_role(username, "developer") + if role == "sysadmin": + nova.remove_user_role(username, "sysadmin") + if role == "netadmin": + nova.remove_user_role(username, "netadmin") + if role == "cloudadmin": + nova.remove_user_role(username, "cloudadmin") + if role == "itsec": + nova.remove_user_role(username, "itsec") + + +@staff_member_required +def project_user(request, project_name, project_user): + nova = get_nova_admin_connection() + userroles = nova.get_user_roles(project_user, project_name) + try: + modeluser = auth_models.User.objects.get(username = project_user) + except auth_models.User.DoesNotExist: + modeluser = None + + if request.method == 'POST': + form = forms.ProjectUserForm(request.POST) + if form.is_valid(): + username = project_user + + # hacky work around to interface correctly with multiple select form + remove_project_roles(username, project_name) + + roleform = request.POST.getlist("role") + for role in roleform: + nova.add_user_role(username, str(role), project_name) + + return redirect('admin_project', project_name) + else: + roles = [str(role.role) for role in userroles] + form = forms.ProjectUserForm({ + 'role': roles, + 'user': modeluser, + }) + + project = nova.get_project(project_name) + + return render_to_response('admin/django_nova/project/project_user.html', { + 'form' : form, + 'project' : project, + 'user': modeluser, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def add_project_user(request, project_name): + nova = get_nova_admin_connection() + + if request.method == 'POST': + form = forms.AddProjectUserForm(request.POST, project=project_name) + if form.is_valid(): + username = form.cleaned_data["username"].username + nova.add_project_member(username, project_name,) + + roleform = request.POST.getlist("role") + for role in roleform: + nova.add_user_role(username, str(role), project_name) + + return redirect('admin_project', project_name) + else: + form = forms.AddProjectUserForm(project=project_name) + + project = nova.get_project(project_name) + + return render_to_response('admin/django_nova/project/add_project_user.html', { + 'form' : form, + 'project' : project, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def delete_project_user(request, project_name, project_user): + nova = get_nova_admin_connection() + + if request.method == 'POST': + nova.remove_project_member(project_user, project_name) + return redirect('admin_project', project_name) + + project = nova.get_project(project_name) + user = nova.get_user(project_user) + + return render_to_response('admin/django_nova/project/delete_project_user.html', { + 'user' : user, + 'project' : project, + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def users_list(request): + nova = get_nova_admin_connection() + users = nova.get_users() + + for user in users: + # NOTE(devcamcar): Temporarily disabled for performance reasons. + #roles = [str(role.role) for role in nova.get_user_roles(user.username)] + roles = [] + user.roles = ", ".join(roles) + + return render_to_response('admin/django_nova/project/user_list.html', { + 'users' : users + }, context_instance = template.RequestContext(request)) + + +@staff_member_required +def user_roles(request, user_id): + nova = get_nova_admin_connection() + userroles = nova.get_user_roles(user_id) + try: + modeluser = auth_models.User.objects.get(username=user_id) + except auth_models.User.DoesNotExist: + modeluser = None + + if request.method == 'POST': + form = forms.GlobalRolesForm(request.POST) + if form.is_valid(): + username = user_id + + # hacky work around to interface correctly with multiple select form + remove_global_roles(username) + + roleform = request.POST.getlist("role") + for role in roleform: + nova.add_user_role(username, str(role)) + + return redirect('admin_user_roles', user_id) + else: + roles = [str(role.role) for role in userroles] + form = forms.GlobalRolesForm({ + 'username': modeluser and modeluser.id or None, + 'role': roles, + }) + + return render_to_response('admin/django_nova/project/global_edit_user.html', { + 'form' : form, + 'user' : modeluser, + }, context_instance = template.RequestContext(request)) + diff --git a/django-nova/src/django_nova/views/credentials.py b/django-nova/src/django_nova/views/credentials.py new file mode 100644 index 00000000..3626e027 --- /dev/null +++ b/django-nova/src/django_nova/views/credentials.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for downloading X509 credentials. Useful when using an invitation +style system for configuring first time users. +""" + +from django import http +from django.conf import settings +from django.shortcuts import render_to_response +from django_nova import models + + +def authorize_credentials(request, auth_token): + """Sends X509 credentials to user if their auth token is valid.""" + auth_token = auth_token.lower() + credentials = models.CredentialsAuthorization.get_by_token(auth_token) + + # NOTE(devcamcar): If nothing returned, then token was bad or has expired. + if not credentials: + return render_to_response('django_nova/credentials/expired.html') + + response = http.HttpResponse(mimetype='application/zip') + response['Content-Disposition'] = \ + 'attachment; filename=%s-%s-%s-x509.zip' % \ + (settings.SITE_NAME, credentials.project, credentials.username) + response.write(credentials.get_zip()) + + return response + diff --git a/django-nova/src/django_nova/views/images.py b/django-nova/src/django_nova/views/images.py new file mode 100644 index 00000000..1d09d2df --- /dev/null +++ b/django-nova/src/django_nova/views/images.py @@ -0,0 +1,229 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova images. +""" + +import boto.exception +import re + +from django import http +from django import template +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render_to_response +from django_nova import exceptions +from django_nova import forms +from django_nova import shortcuts +from django_nova.exceptions import handle_nova_error + + +def _image_lists(images, project_id): + def image_is_project(i): + return i.ownerId == project_id + + def image_is_admin(i): + return i.ownerId in ['admin'] + + def image_is_community(i): + return (not image_is_admin(i)) and (not image_is_project(i)) + + return {'Project Images': filter(image_is_project, images), + '%s Images' % settings.SITE_BRANDING: filter(image_is_admin, + images), + 'Community Images': filter(image_is_community, images)} + + +@login_required +@handle_nova_error +def index(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + images = project.get_images() + + return render_to_response('django_nova/images/index.html', { + 'form': forms.LaunchInstanceForm(project), + 'region': project.region, + 'project': project, + 'image_lists': _image_lists(images, project_id), + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def launch(request, project_id, image_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.LaunchInstanceForm(project, request.POST) + if form.is_valid(): + try: + reservation = project.run_instances( + image_id, + addressing_type='private', + key_name=form.cleaned_data['key_name'], + #security_groups=[form.cleaned_data['security_group']], + user_data=re.sub('\r\n', '\n', + form.cleaned_data['user_data']), + instance_type=form.cleaned_data['size'], + min_count=form.cleaned_data['count'], + max_count=form.cleaned_data['count'] + ) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to launch: %s' % e.message) + else: + for instance in reservation.instances: + messages.success(request, + 'Instance %s launched.' % instance.id) + return redirect('nova_instances', project_id) + else: + form = forms.LaunchInstanceForm(project) + + ami = project.get_image(image_id) + + return render_to_response('django_nova/images/launch.html', { + 'form': form, + 'region': project.region, + 'project': project, + 'ami': ami, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def detail(request, project_id, image_id): + project = shortcuts.get_project_or_404(request, project_id) + images = project.get_images() + + ami = project.get_image(image_id) + can_modify = shortcuts.get_user_image_permissions(request.user.username, + project_id) + + if not ami: + raise http.Http404() + return render_to_response('django_nova/images/index.html', { + 'form': forms.LaunchInstanceForm(project), + 'region': project.region, + 'project': project, + 'images': images, + 'image_lists': _image_lists(images, project_id), + 'ami': ami, + 'can_modify': can_modify, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def remove(request, project_id, image_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + try: + project.deregister_image(image_id) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to deregister image: %s' % e.message) + else: + messages.success(request, + 'Image %s has been successfully deregistered.' % + image_id) + + return redirect('nova_images', project_id) + else: + ami = project.get_image(image_id) + + #FIXME - is the code below used? if we reach here should we + #just redirect? (anthony) + return render_to_response('django_nova/images/detail_list.html', { + 'region': project.region, + 'project': project, + 'ami': ami, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def privacy(request, project_id, image_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + ami = project.get_image(image_id) + + if ami.is_public: + try: + project.modify_image_attribute(image_id, + attribute='launchPermission', + operation='remove') + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to make image private: %s' % e.message) + else: + try: + project.modify_image_attribute(image_id, + attribute='launchPermission', + operation='add') + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to make image public: %s' % e.message) + + return redirect('nova_images_detail', project_id, image_id) + + +@login_required +@handle_nova_error +def update(request, project_id, image_id): + project = shortcuts.get_project_or_404(request, project_id) + ami = project.get_image(image_id) + + if request.method == 'POST': + form = forms.UpdateImageForm(ami, request.POST) + if form.is_valid(): + try: + project.update_image(image_id, + form.cleaned_data['nickname'], + form.cleaned_data['description']) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to update image: %s' % e.message) + else: + messages.success(request, + 'Image %s has been updated.' % image_id) + + return redirect('nova_images_detail', project_id, image_id) + + # TODO(devcamcar): This needs to be cleaned up. Can make + # one of the render_to_response blocks go away. + else: + form = forms.UpdateImageForm(ami) + return render_to_response('django_nova/images/edit.html', { + 'form': form, + 'region': project.region, + 'project': project, + 'ami': ami, + }, context_instance = template.RequestContext(request)) + else: + form = forms.UpdateImageForm(ami) + return render_to_response('django_nova/images/edit.html', { + 'form': form, + 'region': project.region, + 'project': project, + 'ami': ami, + }, context_instance = template.RequestContext(request)) + diff --git a/django-nova/src/django_nova/views/instances.py b/django-nova/src/django_nova/views/instances.py new file mode 100644 index 00000000..6fe2e0d0 --- /dev/null +++ b/django-nova/src/django_nova/views/instances.py @@ -0,0 +1,203 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova instances. +""" + +from django import http +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 import exceptions +from django_nova import forms as nova_forms +from django_nova import shortcuts +from django_nova.exceptions import handle_nova_error + + +@login_required +@handle_nova_error +def index(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + instances = sorted(project.get_instances(), key=lambda k: k.public_dns_name) + + return render_to_response('django_nova/instances/index.html', { + 'region': project.region, + 'project': project, + 'instances': instances, + 'detail' : False, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def detail(request, project_id, instance_id): + project = shortcuts.get_project_or_404(request, project_id) + instance = project.get_instance(instance_id) + instances = sorted(project.get_instances(), key=lambda k: k.public_dns_name) + + if not instance: + raise http.Http404() + + return render_to_response('django_nova/instances/index.html', { + 'region': project.region, + 'project': project, + 'selected_instance': instance, + 'instances': instances, + 'update_form': nova_forms.UpdateInstanceForm(instance), + 'detail' : True, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def performance(request, project_id, instance_id): + project = shortcuts.get_project_or_404(request, project_id) + instance = project.get_instance(instance_id) + + if not instance: + raise http.Http404() + + return render_to_response('django_nova/instances/performance.html', { + 'region': project.region, + 'project': project, + 'instance': instance, + 'update_form': nova_forms.UpdateInstanceForm(instance), + }, context_instance = template.RequestContext(request)) + + +# TODO(devcamcar): Wrap this in an @ajax decorator. +def refresh(request, project_id): + # TODO(devcamcar): This logic belongs in decorator. + if not request.user.is_authenticated(): + return http.HttpResponseForbidden() + + project = shortcuts.get_project_or_404(request, project_id) + instances = sorted(project.get_instances(), key=lambda k: k.public_dns_name) + + return render_to_response('django_nova/instances/_instances_list.html', { + 'project': project, + 'instances': instances, + }, context_instance = template.RequestContext(request)) + + +@handle_nova_error +def refresh_detail(request, project_id, instance_id): + # TODO(devcamcar): This logic belongs in decorator. + if not request.user.is_authenticated(): + return http.HttpResponseForbidden() + + project = shortcuts.get_project_or_404(request, project_id) + instance = project.get_instance(instance_id) + instances = sorted(project.get_instances(), key=lambda k: k.public_dns_name) + + return render_to_response('django_nova/instances/_instances_list.html', { + 'project': project, + 'selected_instance': instance, + 'instances': instances, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def terminate(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + instance_id = request.POST['instance_id'] + + try: + project.terminate_instance(instance_id) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to terminate %s: %s' % + (instance_id, e.message,)) + else: + messages.success(request, + 'Instance %s has been terminated.' % instance_id) + + return redirect('nova_instances', project_id) + + +@login_required +@handle_nova_error +def console(request, project_id, instance_id): + project = shortcuts.get_project_or_404(request, project_id) + conn = project.get_nova_connection() + console = conn.get_console_output(instance_id) + response = http.HttpResponse(mimetype='text/plain') + response.write(console.output) + response.flush() + + return response + + +@login_required +@handle_nova_error +def graph(request, project_id, instance_id, graph_name): + project = shortcuts.get_project_or_404(request, project_id) + graph = project.get_instance_graph(instance_id, graph_name) + + if graph is None: + raise http.Http404() + + response = http.HttpResponse(mimetype='image/png') + response.write(graph) + + return response + + +@login_required +@handle_nova_error +def update(request, project_id, instance_id): + project = shortcuts.get_project_or_404(request, project_id) + instance = project.get_instance(instance_id) + + if not instance: + raise http.Http404() + + if request.method == 'POST': + form = nova_forms.UpdateInstanceForm(instance, request.POST) + if form.is_valid(): + try: + project.update_instance(instance_id, form.cleaned_data) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to update instance %s: %s' % + (instance_id, e.message,)) + else: + messages.success(request, + 'Instance %s has been updated.' % instance_id) + return redirect('nova_instances', project_id) + else: + return render_to_response('django_nova/instances/edit.html', { + 'region': project.region, + 'project': project, + 'instance': instance, + 'update_form': form, + }, context_instance = template.RequestContext(request)) + + else: + return render_to_response('django_nova/instances/edit.html', { + 'region': project.region, + 'project': project, + 'instance': instance, + 'update_form': nova_forms.UpdateInstanceForm(instance), + }, context_instance = template.RequestContext(request)) + diff --git a/django-nova/src/django_nova/views/keypairs.py b/django-nova/src/django_nova/views/keypairs.py new file mode 100644 index 00000000..90db0d2e --- /dev/null +++ b/django-nova/src/django_nova/views/keypairs.py @@ -0,0 +1,122 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova keypairs. +""" + +from django import http +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 import exceptions +from django_nova import forms +from django_nova import shortcuts +from django_nova.exceptions import handle_nova_error + + +@login_required +@handle_nova_error +def index(request, project_id, download_key=None): + project = shortcuts.get_project_or_404(request, project_id) + keypairs = project.get_key_pairs() + + return render_to_response('django_nova/keypairs/index.html', { + 'create_form': forms.CreateKeyPairForm(project), + 'region': project.region, + 'project': project, + 'keypairs': keypairs, + 'download_key': download_key + }, context_instance = template.RequestContext(request)) + +@login_required +@handle_nova_error +def add(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.CreateKeyPairForm(project, request.POST) + + if form.is_valid(): + try: + keypair = project.create_key_pair(form.cleaned_data['name']) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to create key: %s' % e.message) + else: + if request.POST['js'] == '1': + request.session['key.%s' % keypair.name] = keypair.material + return index(request, + project_id, + download_key=keypair.name) + else: + response = http.HttpResponse(mimetype='application/binary') + response['Content-Disposition'] = \ + 'attachment; filename=%s.pem' % \ + form.cleaned_data['name'] + response.write(keypair.material) + return response + else: + keypairs = project.get_key_pairs() + + return render_to_response('django_nova/keypairs/index.html', { + 'create_form': form, + 'region': project.region, + 'project': project, + 'keypairs': keypairs, + }, context_instance = template.RequestContext(request)) + + return redirect('nova_keypairs', project_id) + +@login_required +@handle_nova_error +def delete(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + key_name = request.POST['key_name'] + + try: + project.delete_key_pair(key_name) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to delete key: %s' % e.message) + else: + messages.success(request, + 'Key %s has been successfully deleted.' % \ + key_name) + + return redirect('nova_keypairs', project_id) + +@login_required +@handle_nova_error +def download(request, project_id, key_name): + # Ensure the project exists. + shortcuts.get_project_or_404(request, project_id) + + try: + material = request.session.pop('key.%s' % key_name) + except KeyError: + return redirect('nova_keypairs', project_id) + + response = http.HttpResponse(mimetype='application/binary') + response['Content-Disposition'] = 'attachment; filename=%s.pem' % key_name + response.write(material) + + return response diff --git a/django-nova/src/django_nova/views/projects.py b/django-nova/src/django_nova/views/projects.py new file mode 100644 index 00000000..b2a217c7 --- /dev/null +++ b/django-nova/src/django_nova/views/projects.py @@ -0,0 +1,107 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova projects. +""" + +from django import http +from django import template +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render_to_response +from django_nova import forms as nova_forms +from django_nova.connection import get_nova_admin_connection +from django_nova.exceptions import handle_nova_error +from django_nova.shortcuts import get_project_or_404 + + +@login_required +@handle_nova_error +def detail(request, project_id): + project = get_project_or_404(request, project_id) + + return render_to_response('django_nova/projects/index.html', { + 'project': project, + 'instance_count': project.get_instance_count(), + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def manage(request, project_id): + project = get_project_or_404(request, project_id) + + if project.projectManagerId != request.user.username: + return redirect('login') + + nova = get_nova_admin_connection() + members = nova.get_project_members(project_id) + + for member in members: + project_role = [str(role.role) for role in nova.get_user_roles(member.memberId, project_id)] + global_role = [str(role.role) for role in nova.get_user_roles(member.memberId, project=False)] + + member.project_roles = ", ".join(project_role) + member.global_roles = ", ".join(global_role) + + + return render_to_response('django_nova/projects/manage.html', { + 'project': project, + 'members': members, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def edit_user(request, project_id, username): + nova = get_nova_admin_connection() + project = get_project_or_404(request, project_id) + user = nova.get_user(username) + + if project.projectManagerId != request.user.username: + return redirect('login') + + if request.method == 'POST': + form = nova_forms.ProjectUserForm(project, user, request.POST) + if form.is_valid(): + form.save() + + return redirect('nova_project_manage', project_id) + else: + form = nova_forms.ProjectUserForm(project, user) + + return render_to_response('django_nova/projects/edit_user.html', { + 'form' : form, + 'project': project, + 'user' : user, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def download_credentials(request, project_id): + project = get_project_or_404(request, project_id) + + response = http.HttpResponse(mimetype='application/zip') + response['Content-Disposition'] = \ + 'attachment; filename=%s-%s-%s-x509.zip' % \ + (settings.SITE_NAME, project.projectname, request.user) + response.write(project.get_zip()) + + return response diff --git a/django-nova/src/django_nova/views/regions.py b/django-nova/src/django_nova/views/regions.py new file mode 100644 index 00000000..18914276 --- /dev/null +++ b/django-nova/src/django_nova/views/regions.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova regions. +""" + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect +from django_nova.shortcuts import set_current_region + + +@login_required +def change(request): + region = request.POST['region'] + redirect_url = request.POST['redirect_url'] + set_current_region(request, region) + messages.success(request, 'You are now using the region "%s".' % region) + + return redirect(redirect_url) diff --git a/django-nova/src/django_nova/views/securitygroups.py b/django-nova/src/django_nova/views/securitygroups.py new file mode 100644 index 00000000..6f85b4af --- /dev/null +++ b/django-nova/src/django_nova/views/securitygroups.py @@ -0,0 +1,180 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova security groups. +""" + +from django import http +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 import exceptions +from django_nova import forms +from django_nova.exceptions import handle_nova_error +from django_nova.shortcuts import get_project_or_404 + + +@login_required +@handle_nova_error +def index(request, project_id): + project = get_project_or_404(request, project_id) + securitygroups = project.get_security_groups() + + return render_to_response('django_nova/securitygroups/index.html', { + 'create_form': forms.CreateSecurityGroupForm(project), + 'project': project, + 'securitygroups': securitygroups, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def detail(request, project_id, group_name): + project = get_project_or_404(request, project_id) + securitygroup = project.get_security_group(group_name) + + if not securitygroup: + raise http.Http404 + + return render_to_response('django_nova/securitygroups/detail.html', { + 'authorize_form': forms.AuthorizeSecurityGroupRuleForm(), + 'project': project, + 'securitygroup': securitygroup, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def add(request, project_id): + project = get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.CreateSecurityGroupForm(project, request.POST) + if form.is_valid(): + try: + project.create_security_group( + form.cleaned_data['name'], + form.cleaned_data['description']) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to create security group: %s' % e.message) + else: + messages.success( + request, + 'Security Group %s has been succesfully created.' % \ + form.cleaned_data['name']) + else: + securitygroups = project.get_security_groups() + + return render_to_response('django_nova/securitygroups/index.html', { + 'create_form': form, + 'project': project, + 'securitygroups': securitygroups, + }, context_instance = template.RequestContext(request)) + + return redirect('nova_securitygroups', project_id) + + +@login_required +@handle_nova_error +def authorize(request, project_id, group_name): + project = get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.AuthorizeSecurityGroupRuleForm(request.POST) + if form.is_valid(): + try: + project.authorize_security_group( + group_name = group_name, + ip_protocol = form.cleaned_data['protocol'], + from_port = form.cleaned_data['from_port'], + to_port = form.cleaned_data['to_port']) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to authorize: %s' % e.message) + else: + messages.success( + request, + 'Security Group %s: Access to %s ports %d - %d' + ' has been authorized.' % + (group_name, + form.cleaned_data['protocol'], + form.cleaned_data['from_port'], + form.cleaned_data['to_port'])) + else: + securitygroup = project.get_security_group(group_name) + + if not securitygroup: + raise http.Http404 + + return render_to_response('django_nova/securitygroups/detail.html', { + 'authorize_form': form, + 'project': project, + 'securitygroup': securitygroup, + }, context_instance = template.RequestContext(request)) + + return redirect('nova_securitygroups_detail', project_id, group_name) + + +@login_required +@handle_nova_error +def revoke(request, project_id, group_name): + project = get_project_or_404(request, project_id) + + if request.method == 'POST': + try: + project.revoke_security_group( + group_name = group_name, + ip_protocol = request.POST['protocol'], + from_port = request.POST['from_port'], + to_port = request.POST['to_port']) + except exceptions.NovaApiError, e: + messages.error(request, 'Unable to revoke: %s' % e.message) + else: + messages.success( + request, + 'Security Group %s: Access to %s ports %s - %s ' + 'has been revoked.' % + (group_name, + request.POST['protocol'], + request.POST['from_port'], + request.POST['to_port'])) + + return redirect('nova_securitygroups_detail', project_id, group_name) + + +@login_required +@handle_nova_error +def delete(request, project_id, group_name): + project = get_project_or_404(request, project_id) + + if request.method == 'POST': + try: + project.delete_security_group(name=group_name) + except exceptions.NovaApiError, e: + messages.error( + request, + 'Unable to delete security group: %s' % e.message) + else: + messages.success(request, + 'Security Group %s was successfully deleted.' % + group_name) + + return redirect('nova_securitygroups', project_id) diff --git a/django-nova/src/django_nova/views/volumes.py b/django-nova/src/django_nova/views/volumes.py new file mode 100644 index 00000000..9d623ed8 --- /dev/null +++ b/django-nova/src/django_nova/views/volumes.py @@ -0,0 +1,151 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Views for managing Nova volumes. +""" + +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 import exceptions +from django_nova import forms +from django_nova import shortcuts +from django_nova.exceptions import handle_nova_error + + +@login_required +@handle_nova_error +def index(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + volumes = project.get_volumes() + + return render_to_response('django_nova/volumes/index.html', { + 'create_form': forms.CreateVolumeForm(), + 'attach_form': forms.AttachVolumeForm(project), + 'region': project.region, + 'project': project, + 'volumes': volumes, + }, context_instance = template.RequestContext(request)) + + +@login_required +@handle_nova_error +def add(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.CreateVolumeForm(request.POST) + if form.is_valid(): + try: + volume = project.create_volume(form.cleaned_data['size'], + form.cleaned_data['nickname'], + form.cleaned_data['description']) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to create volume: %s' % e.message) + else: + messages.success(request, + 'Volume %s %s has been successfully created.' % + (volume.id, volume.displayName)) + else: + volumes = project.get_volumes() + + return render_to_response('django_nova/volumes/index.html', { + 'create_form': form, + 'attach_form': forms.AttachVolumeForm(project), + 'region': project.region, + 'project': project, + 'volumes': volumes, + }, context_instance = template.RequestContext(request)) + + return redirect('nova_volumes', project_id) + + +@login_required +@handle_nova_error +def delete(request, project_id, volume_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + try: + project.delete_volume(volume_id) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to delete volume: %s' % e.message) + else: + messages.success(request, + 'Volume %s has been successfully deleted.' + % volume_id) + + return redirect('nova_volumes', project_id) + + +@login_required +@handle_nova_error +def attach(request, project_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + form = forms.AttachVolumeForm(project, request.POST) + if form.is_valid(): + try: + project.attach_volume( + form.cleaned_data['volume'], + form.cleaned_data['instance'], + form.cleaned_data['device'] + ) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to attach volume: %s' % e.message) + else: + messages.success(request, + 'Volume %s has been successfully attached.' % + form.cleaned_data['volume']) + else: + volumes = project.get_volumes() + + return render_to_response('django_nova/volumes/index.html', { + 'create_form': forms.CreateVolumeForm(), + 'attach_form': form, + 'region': project.region, + 'project': project, + 'volumes': volumes, + }, context_instance = template.RequestContext(request)) + + return redirect('nova_volumes', project_id) + + +@login_required +@handle_nova_error +def detach(request, project_id, volume_id): + project = shortcuts.get_project_or_404(request, project_id) + + if request.method == 'POST': + try: + project.detach_volume(volume_id) + except exceptions.NovaApiError, e: + messages.error(request, + 'Unable to detach volume: %s' % e.message) + else: + messages.success(request, + 'Volume %s has been successfully detached.' % + volume_id) + + return redirect('nova_volumes', project_id) diff --git a/openstack-dashboard/README b/openstack-dashboard/README new file mode 100644 index 00000000..3856aca7 --- /dev/null +++ b/openstack-dashboard/README @@ -0,0 +1,49 @@ +OpenStack Dashboard +------------------- + +The OpenStack Dashboard is a reference implementation of a Django site that +uses the Django-Nova project to provide web based interactions with the +OpenStack Nova cloud controller. + +For more information about the Django-Nova project, please visit: + + http://launchpad.net/django-nova + + +Getting Started +--------------- + +The first step is to obtain a local copy of the django-nova project: + + $ mkdir django-nova + $ cd django-nova + $ bzr init-repo . + $ bzr branch lp:django-nova/trunk + + +Next we will create the virtualenv for local development. A tool is included to +create one for you: + + $ python tools/install_venv.py + + +Now that the virtualenv is created, you need to configure your local +environment. To do this, create a local_settings.py file in the local/ +directory. There is a local_settings.py.example file there that may be used +as a template. + +Finally, issue the django syncdb command: + + $ tools/with_venv.sh dashboard/manage.py syncdb + +If after you have specified the admin user the script appears to hang, it +probably means the installation of Nova being referred to in local_settings.py +is unavailable. + + +If all is well you should now able to run the server locally: + + $ tools/with_venv.sh dashboard/manage.py runserver + + + diff --git a/openstack-dashboard/dashboard/__init__.py b/openstack-dashboard/dashboard/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstack-dashboard/dashboard/__init__.py diff --git a/dashboard/manage.py b/openstack-dashboard/dashboard/manage.py index 5e78ea97..5e78ea97 100755 --- a/dashboard/manage.py +++ b/openstack-dashboard/dashboard/manage.py diff --git a/dashboard/settings.py b/openstack-dashboard/dashboard/settings.py index 2a374212..2a374212 100644 --- a/dashboard/settings.py +++ b/openstack-dashboard/dashboard/settings.py diff --git a/dashboard/templates/403.html b/openstack-dashboard/dashboard/templates/403.html index 14e0a875..14e0a875 100644 --- a/dashboard/templates/403.html +++ b/openstack-dashboard/dashboard/templates/403.html diff --git a/dashboard/templates/404.html b/openstack-dashboard/dashboard/templates/404.html index 53aabfa1..53aabfa1 100644 --- a/dashboard/templates/404.html +++ b/openstack-dashboard/dashboard/templates/404.html diff --git a/dashboard/templates/500.html b/openstack-dashboard/dashboard/templates/500.html index 228be539..228be539 100644 --- a/dashboard/templates/500.html +++ b/openstack-dashboard/dashboard/templates/500.html diff --git a/dashboard/templates/admin/index.html b/openstack-dashboard/dashboard/templates/admin/index.html index ddb79a8c..ddb79a8c 100644 --- a/dashboard/templates/admin/index.html +++ b/openstack-dashboard/dashboard/templates/admin/index.html diff --git a/dashboard/templates/base-root.html b/openstack-dashboard/dashboard/templates/base-root.html index 7454a342..7454a342 100644 --- a/dashboard/templates/base-root.html +++ b/openstack-dashboard/dashboard/templates/base-root.html diff --git a/dashboard/templates/base-sidebar.html b/openstack-dashboard/dashboard/templates/base-sidebar.html index d7d0021d..d7d0021d 100644 --- a/dashboard/templates/base-sidebar.html +++ b/openstack-dashboard/dashboard/templates/base-sidebar.html diff --git a/dashboard/templates/base.html b/openstack-dashboard/dashboard/templates/base.html index cf37c0e4..cf37c0e4 100644 --- a/dashboard/templates/base.html +++ b/openstack-dashboard/dashboard/templates/base.html diff --git a/dashboard/templates/index.html b/openstack-dashboard/dashboard/templates/index.html index ba99a5d1..14820687 100644 --- a/dashboard/templates/index.html +++ b/openstack-dashboard/dashboard/templates/index.html @@ -36,8 +36,8 @@ <h3>OpenStack Resources</h3> <ul> <li><a href="http://openstack.org" target="_blank">OpenStack.org</a></li> + <li><a href="http://wiki.openstack.org/OpenStackDashboard" target="_blank">OpenStack Dashboard Wiki</a></li> <li><a href="https://launchpad.net/openstack-dashboard" target="_blank">OpenStack Dashboard Launchpad Repository</a></li> - <li><a href="https://launchpad.net/django-nova" target="_blank">Django-Nova Launchpad Repository</a></li> </ul> </div> @@ -69,8 +69,8 @@ <h3>OpenStack Resources</h3> <ul> <li><a href="http://openstack.org" target="_blank">OpenStack.org</a></li> + <li><a href="http://wiki.openstack.org/OpenStackDashboard" target="_blank">OpenStack Dashboard Wiki</a></li> <li><a href="https://launchpad.net/openstack-dashboard" target="_blank">OpenStack Dashboard Launchpad Repository</a></li> - <li><a href="https://launchpad.net/django-nova" target="_blank">Django-Nova Launchpad Repository</a></li> </ul> </div> {% endif %} diff --git a/dashboard/templates/permission_denied.html b/openstack-dashboard/dashboard/templates/permission_denied.html index 96b02dcc..96b02dcc 100644 --- a/dashboard/templates/permission_denied.html +++ b/openstack-dashboard/dashboard/templates/permission_denied.html diff --git a/dashboard/templates/registration/activate.html b/openstack-dashboard/dashboard/templates/registration/activate.html index e6ffdc9e..e6ffdc9e 100644 --- a/dashboard/templates/registration/activate.html +++ b/openstack-dashboard/dashboard/templates/registration/activate.html diff --git a/dashboard/templates/registration/activation_email.txt b/openstack-dashboard/dashboard/templates/registration/activation_email.txt index c9059ec0..c9059ec0 100644 --- a/dashboard/templates/registration/activation_email.txt +++ b/openstack-dashboard/dashboard/templates/registration/activation_email.txt diff --git a/dashboard/templates/registration/activation_email_subject.txt b/openstack-dashboard/dashboard/templates/registration/activation_email_subject.txt index b0472aea..b0472aea 100644 --- a/dashboard/templates/registration/activation_email_subject.txt +++ b/openstack-dashboard/dashboard/templates/registration/activation_email_subject.txt diff --git a/dashboard/templates/registration/login.html b/openstack-dashboard/dashboard/templates/registration/login.html index 2e85c7ed..2e85c7ed 100644 --- a/dashboard/templates/registration/login.html +++ b/openstack-dashboard/dashboard/templates/registration/login.html diff --git a/dashboard/templates/registration/logout.html b/openstack-dashboard/dashboard/templates/registration/logout.html index a97e3859..a97e3859 100644 --- a/dashboard/templates/registration/logout.html +++ b/openstack-dashboard/dashboard/templates/registration/logout.html diff --git a/dashboard/templates/registration/password_change_done.html b/openstack-dashboard/dashboard/templates/registration/password_change_done.html index 852ea526..852ea526 100644 --- a/dashboard/templates/registration/password_change_done.html +++ b/openstack-dashboard/dashboard/templates/registration/password_change_done.html diff --git a/dashboard/templates/registration/password_change_form.html b/openstack-dashboard/dashboard/templates/registration/password_change_form.html index 70bf79c2..70bf79c2 100644 --- a/dashboard/templates/registration/password_change_form.html +++ b/openstack-dashboard/dashboard/templates/registration/password_change_form.html diff --git a/dashboard/templates/registration/password_reset_complete.html b/openstack-dashboard/dashboard/templates/registration/password_reset_complete.html index dc972451..dc972451 100644 --- a/dashboard/templates/registration/password_reset_complete.html +++ b/openstack-dashboard/dashboard/templates/registration/password_reset_complete.html diff --git a/dashboard/templates/registration/password_reset_confirm.html b/openstack-dashboard/dashboard/templates/registration/password_reset_confirm.html index acd9af6e..acd9af6e 100644 --- a/dashboard/templates/registration/password_reset_confirm.html +++ b/openstack-dashboard/dashboard/templates/registration/password_reset_confirm.html diff --git a/dashboard/templates/registration/password_reset_done.html b/openstack-dashboard/dashboard/templates/registration/password_reset_done.html index 9aed573a..9aed573a 100644 --- a/dashboard/templates/registration/password_reset_done.html +++ b/openstack-dashboard/dashboard/templates/registration/password_reset_done.html diff --git a/dashboard/templates/registration/password_reset_email.html b/openstack-dashboard/dashboard/templates/registration/password_reset_email.html index cd75e305..cd75e305 100644 --- a/dashboard/templates/registration/password_reset_email.html +++ b/openstack-dashboard/dashboard/templates/registration/password_reset_email.html diff --git a/dashboard/templates/registration/password_reset_form.html b/openstack-dashboard/dashboard/templates/registration/password_reset_form.html index 06461f1f..06461f1f 100644 --- a/dashboard/templates/registration/password_reset_form.html +++ b/openstack-dashboard/dashboard/templates/registration/password_reset_form.html diff --git a/dashboard/templates/registration/registration_complete.html b/openstack-dashboard/dashboard/templates/registration/registration_complete.html index c5c0e97f..c5c0e97f 100644 --- a/dashboard/templates/registration/registration_complete.html +++ b/openstack-dashboard/dashboard/templates/registration/registration_complete.html diff --git a/dashboard/templates/registration/registration_form.html b/openstack-dashboard/dashboard/templates/registration/registration_form.html index 747d8684..747d8684 100644 --- a/dashboard/templates/registration/registration_form.html +++ b/openstack-dashboard/dashboard/templates/registration/registration_form.html diff --git a/dashboard/templates/unavailable.html b/openstack-dashboard/dashboard/templates/unavailable.html index 5568bc4e..5568bc4e 100644 --- a/dashboard/templates/unavailable.html +++ b/openstack-dashboard/dashboard/templates/unavailable.html diff --git a/dashboard/urls.py b/openstack-dashboard/dashboard/urls.py index 485fe031..485fe031 100644 --- a/dashboard/urls.py +++ b/openstack-dashboard/dashboard/urls.py diff --git a/dashboard/views.py b/openstack-dashboard/dashboard/views.py index 28bcefc9..28bcefc9 100644 --- a/dashboard/views.py +++ b/openstack-dashboard/dashboard/views.py diff --git a/dashboard/wsgi/django.wsgi b/openstack-dashboard/dashboard/wsgi/django.wsgi index 9c821ecb..9c821ecb 100644 --- a/dashboard/wsgi/django.wsgi +++ b/openstack-dashboard/dashboard/wsgi/django.wsgi diff --git a/openstack-dashboard/local/__init__.py b/openstack-dashboard/local/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstack-dashboard/local/__init__.py diff --git a/local/local_settings.py.example b/openstack-dashboard/local/local_settings.py.example index 95631bc2..95631bc2 100644 --- a/local/local_settings.py.example +++ b/openstack-dashboard/local/local_settings.py.example diff --git a/media/dashboard/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png Binary files differindex 6348115e..6348115e 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png Binary files differindex 7680b543..7680b543 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png Binary files differindex 5ae77ce6..5ae77ce6 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png Binary files differindex baabca6b..baabca6b 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png Binary files differindex d9387e95..d9387e95 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png Binary files differindex 28b566c2..28b566c2 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png Binary files differindex d5882978..d5882978 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png Binary files differindex 2289d3c7..2289d3c7 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png diff --git a/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png Binary files differindex 0de3275b..0de3275b 100644 --- a/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png diff --git a/media/dashboard/css/cupertino/images/ui-icons_2694e8_256x240.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_2694e8_256x240.png Binary files differindex bdc74718..bdc74718 100644 --- a/media/dashboard/css/cupertino/images/ui-icons_2694e8_256x240.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_2694e8_256x240.png diff --git a/media/dashboard/css/cupertino/images/ui-icons_2e83ff_256x240.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_2e83ff_256x240.png Binary files differindex 45e8928e..45e8928e 100644 --- a/media/dashboard/css/cupertino/images/ui-icons_2e83ff_256x240.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_2e83ff_256x240.png diff --git a/media/dashboard/css/cupertino/images/ui-icons_3d80b3_256x240.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_3d80b3_256x240.png Binary files differindex 76a020d9..76a020d9 100644 --- a/media/dashboard/css/cupertino/images/ui-icons_3d80b3_256x240.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_3d80b3_256x240.png diff --git a/media/dashboard/css/cupertino/images/ui-icons_72a7cf_256x240.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_72a7cf_256x240.png Binary files differindex 0e8f4d9d..0e8f4d9d 100644 --- a/media/dashboard/css/cupertino/images/ui-icons_72a7cf_256x240.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_72a7cf_256x240.png diff --git a/media/dashboard/css/cupertino/images/ui-icons_ffffff_256x240.png b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_ffffff_256x240.png Binary files differindex bef5178a..bef5178a 100644 --- a/media/dashboard/css/cupertino/images/ui-icons_ffffff_256x240.png +++ b/openstack-dashboard/media/dashboard/css/cupertino/images/ui-icons_ffffff_256x240.png diff --git a/media/dashboard/css/cupertino/jquery-ui-1.7.2.custom.css b/openstack-dashboard/media/dashboard/css/cupertino/jquery-ui-1.7.2.custom.css index 6b3ee7a8..6b3ee7a8 100644 --- a/media/dashboard/css/cupertino/jquery-ui-1.7.2.custom.css +++ b/openstack-dashboard/media/dashboard/css/cupertino/jquery-ui-1.7.2.custom.css diff --git a/media/dashboard/css/django-admin-widgets.css b/openstack-dashboard/media/dashboard/css/django-admin-widgets.css index 43521be0..43521be0 100644 --- a/media/dashboard/css/django-admin-widgets.css +++ b/openstack-dashboard/media/dashboard/css/django-admin-widgets.css diff --git a/media/dashboard/css/ie7.css b/openstack-dashboard/media/dashboard/css/ie7.css index e29f06fb..e29f06fb 100755 --- a/media/dashboard/css/ie7.css +++ b/openstack-dashboard/media/dashboard/css/ie7.css diff --git a/media/dashboard/css/openstack.css b/openstack-dashboard/media/dashboard/css/openstack.css index dfbe3797..dfbe3797 100644 --- a/media/dashboard/css/openstack.css +++ b/openstack-dashboard/media/dashboard/css/openstack.css diff --git a/media/dashboard/css/reset.css b/openstack-dashboard/media/dashboard/css/reset.css index 26cd169c..26cd169c 100644 --- a/media/dashboard/css/reset.css +++ b/openstack-dashboard/media/dashboard/css/reset.css diff --git a/media/dashboard/img/body_bg.gif b/openstack-dashboard/media/dashboard/img/body_bg.gif Binary files differindex e45adc52..e45adc52 100644 --- a/media/dashboard/img/body_bg.gif +++ b/openstack-dashboard/media/dashboard/img/body_bg.gif diff --git a/media/dashboard/img/body_bg.png b/openstack-dashboard/media/dashboard/img/body_bg.png Binary files differindex e4625ec0..e4625ec0 100755 --- a/media/dashboard/img/body_bg.png +++ b/openstack-dashboard/media/dashboard/img/body_bg.png diff --git a/media/dashboard/img/book_icon.png b/openstack-dashboard/media/dashboard/img/book_icon.png Binary files differindex bd41e7de..bd41e7de 100644 --- a/media/dashboard/img/book_icon.png +++ b/openstack-dashboard/media/dashboard/img/book_icon.png diff --git a/media/dashboard/img/bread_crumb.gif b/openstack-dashboard/media/dashboard/img/bread_crumb.gif Binary files differindex 49d92810..49d92810 100644 --- a/media/dashboard/img/bread_crumb.gif +++ b/openstack-dashboard/media/dashboard/img/bread_crumb.gif diff --git a/media/dashboard/img/btn_bg.png b/openstack-dashboard/media/dashboard/img/btn_bg.png Binary files differindex 00227dba..00227dba 100644 --- a/media/dashboard/img/btn_bg.png +++ b/openstack-dashboard/media/dashboard/img/btn_bg.png diff --git a/media/dashboard/img/chat_icon.png b/openstack-dashboard/media/dashboard/img/chat_icon.png Binary files differindex a905633f..a905633f 100644 --- a/media/dashboard/img/chat_icon.png +++ b/openstack-dashboard/media/dashboard/img/chat_icon.png diff --git a/media/dashboard/img/content_bg.gif b/openstack-dashboard/media/dashboard/img/content_bg.gif Binary files differindex e77a90c5..e77a90c5 100644 --- a/media/dashboard/img/content_bg.gif +++ b/openstack-dashboard/media/dashboard/img/content_bg.gif diff --git a/media/dashboard/img/content_shadow.png b/openstack-dashboard/media/dashboard/img/content_shadow.png Binary files differindex 658a725f..658a725f 100644 --- a/media/dashboard/img/content_shadow.png +++ b/openstack-dashboard/media/dashboard/img/content_shadow.png diff --git a/media/dashboard/img/dashboard_nav_bg.png b/openstack-dashboard/media/dashboard/img/dashboard_nav_bg.png Binary files differindex f0e21bd3..f0e21bd3 100644 --- a/media/dashboard/img/dashboard_nav_bg.png +++ b/openstack-dashboard/media/dashboard/img/dashboard_nav_bg.png diff --git a/media/dashboard/img/foot_back.png b/openstack-dashboard/media/dashboard/img/foot_back.png Binary files differindex 13f81049..13f81049 100644 --- a/media/dashboard/img/foot_back.png +++ b/openstack-dashboard/media/dashboard/img/foot_back.png diff --git a/media/dashboard/img/gears.png b/openstack-dashboard/media/dashboard/img/gears.png Binary files differindex a25cadbe..a25cadbe 100644 --- a/media/dashboard/img/gears.png +++ b/openstack-dashboard/media/dashboard/img/gears.png diff --git a/media/dashboard/img/header_bg.png b/openstack-dashboard/media/dashboard/img/header_bg.png Binary files differindex b1b926be..b1b926be 100755 --- a/media/dashboard/img/header_bg.png +++ b/openstack-dashboard/media/dashboard/img/header_bg.png diff --git a/media/dashboard/img/home_head_back.png b/openstack-dashboard/media/dashboard/img/home_head_back.png Binary files differindex 6ebaca18..6ebaca18 100644 --- a/media/dashboard/img/home_head_back.png +++ b/openstack-dashboard/media/dashboard/img/home_head_back.png diff --git a/media/dashboard/img/image_detail.png b/openstack-dashboard/media/dashboard/img/image_detail.png Binary files differindex 054eeff7..054eeff7 100644 --- a/media/dashboard/img/image_detail.png +++ b/openstack-dashboard/media/dashboard/img/image_detail.png diff --git a/media/dashboard/img/logged_in_box_bg.gif b/openstack-dashboard/media/dashboard/img/logged_in_box_bg.gif Binary files differindex ddd950d5..ddd950d5 100644 --- a/media/dashboard/img/logged_in_box_bg.gif +++ b/openstack-dashboard/media/dashboard/img/logged_in_box_bg.gif diff --git a/media/dashboard/img/login_bg.png b/openstack-dashboard/media/dashboard/img/login_bg.png Binary files differindex 3928fb42..3928fb42 100644 --- a/media/dashboard/img/login_bg.png +++ b/openstack-dashboard/media/dashboard/img/login_bg.png diff --git a/media/dashboard/img/login_btn.png b/openstack-dashboard/media/dashboard/img/login_btn.png Binary files differindex fe5a1ac0..fe5a1ac0 100644 --- a/media/dashboard/img/login_btn.png +++ b/openstack-dashboard/media/dashboard/img/login_btn.png diff --git a/media/dashboard/img/logo.gif b/openstack-dashboard/media/dashboard/img/logo.gif Binary files differindex 8da127c3..8da127c3 100644 --- a/media/dashboard/img/logo.gif +++ b/openstack-dashboard/media/dashboard/img/logo.gif diff --git a/media/dashboard/img/nav_arrow.png b/openstack-dashboard/media/dashboard/img/nav_arrow.png Binary files differindex 46c113cd..46c113cd 100644 --- a/media/dashboard/img/nav_arrow.png +++ b/openstack-dashboard/media/dashboard/img/nav_arrow.png diff --git a/media/dashboard/img/nav_bg.png b/openstack-dashboard/media/dashboard/img/nav_bg.png Binary files differindex 90ae6dc0..90ae6dc0 100644 --- a/media/dashboard/img/nav_bg.png +++ b/openstack-dashboard/media/dashboard/img/nav_bg.png diff --git a/media/dashboard/img/nav_highlight.png b/openstack-dashboard/media/dashboard/img/nav_highlight.png Binary files differindex b562b1c3..b562b1c3 100755 --- a/media/dashboard/img/nav_highlight.png +++ b/openstack-dashboard/media/dashboard/img/nav_highlight.png diff --git a/media/dashboard/img/page_header.png b/openstack-dashboard/media/dashboard/img/page_header.png Binary files differindex dac79491..dac79491 100644 --- a/media/dashboard/img/page_header.png +++ b/openstack-dashboard/media/dashboard/img/page_header.png diff --git a/media/dashboard/img/projects_bg.png b/openstack-dashboard/media/dashboard/img/projects_bg.png Binary files differindex cd52bf15..cd52bf15 100644 --- a/media/dashboard/img/projects_bg.png +++ b/openstack-dashboard/media/dashboard/img/projects_bg.png diff --git a/media/dashboard/img/server_icon.png b/openstack-dashboard/media/dashboard/img/server_icon.png Binary files differindex 855e57ce..855e57ce 100644 --- a/media/dashboard/img/server_icon.png +++ b/openstack-dashboard/media/dashboard/img/server_icon.png diff --git a/media/dashboard/img/spinner.gif b/openstack-dashboard/media/dashboard/img/spinner.gif Binary files differindex 42832024..42832024 100644 --- a/media/dashboard/img/spinner.gif +++ b/openstack-dashboard/media/dashboard/img/spinner.gif diff --git a/media/dashboard/img/sub-head-back.png b/openstack-dashboard/media/dashboard/img/sub-head-back.png Binary files differindex b4aacbd5..b4aacbd5 100644 --- a/media/dashboard/img/sub-head-back.png +++ b/openstack-dashboard/media/dashboard/img/sub-head-back.png diff --git a/media/dashboard/img/sub_head_back.png b/openstack-dashboard/media/dashboard/img/sub_head_back.png Binary files differindex 3903dcec..3903dcec 100644 --- a/media/dashboard/img/sub_head_back.png +++ b/openstack-dashboard/media/dashboard/img/sub_head_back.png diff --git a/media/dashboard/img/table_header_bg.png b/openstack-dashboard/media/dashboard/img/table_header_bg.png Binary files differindex 06eb3dee..06eb3dee 100644 --- a/media/dashboard/img/table_header_bg.png +++ b/openstack-dashboard/media/dashboard/img/table_header_bg.png diff --git a/media/dashboard/img/table_heading_bg.png b/openstack-dashboard/media/dashboard/img/table_heading_bg.png Binary files differindex 6fcba877..6fcba877 100644 --- a/media/dashboard/img/table_heading_bg.png +++ b/openstack-dashboard/media/dashboard/img/table_heading_bg.png diff --git a/media/dashboard/img/title-blank-short-foot.png b/openstack-dashboard/media/dashboard/img/title-blank-short-foot.png Binary files differindex 548ce450..548ce450 100644 --- a/media/dashboard/img/title-blank-short-foot.png +++ b/openstack-dashboard/media/dashboard/img/title-blank-short-foot.png diff --git a/media/dashboard/js/dashboard.js b/openstack-dashboard/media/dashboard/js/dashboard.js index 4559d6c3..4559d6c3 100644 --- a/media/dashboard/js/dashboard.js +++ b/openstack-dashboard/media/dashboard/js/dashboard.js diff --git a/media/dashboard/js/django-admin.multiselect.js b/openstack-dashboard/media/dashboard/js/django-admin.multiselect.js index f91cdb53..f91cdb53 100644 --- a/media/dashboard/js/django-admin.multiselect.js +++ b/openstack-dashboard/media/dashboard/js/django-admin.multiselect.js diff --git a/media/dashboard/js/jquery-ui.min.js b/openstack-dashboard/media/dashboard/js/jquery-ui.min.js index 3e168d10..3e168d10 100755 --- a/media/dashboard/js/jquery-ui.min.js +++ b/openstack-dashboard/media/dashboard/js/jquery-ui.min.js diff --git a/media/dashboard/js/jquery.form.js b/openstack-dashboard/media/dashboard/js/jquery.form.js index be8c0b6b..be8c0b6b 100644 --- a/media/dashboard/js/jquery.form.js +++ b/openstack-dashboard/media/dashboard/js/jquery.form.js diff --git a/media/dashboard/js/jquery.min.js b/openstack-dashboard/media/dashboard/js/jquery.min.js index 7c243080..7c243080 100644 --- a/media/dashboard/js/jquery.min.js +++ b/openstack-dashboard/media/dashboard/js/jquery.min.js diff --git a/run_tests.sh b/openstack-dashboard/run_tests.sh index 38499f09..38499f09 100755 --- a/run_tests.sh +++ b/openstack-dashboard/run_tests.sh diff --git a/tools/install_venv.py b/openstack-dashboard/tools/install_venv.py index 987e8b88..8e90e60c 100644 --- a/tools/install_venv.py +++ b/openstack-dashboard/tools/install_venv.py @@ -32,6 +32,7 @@ VENV = os.path.join(ROOT, '.dashboard-venv') WITH_VENV = os.path.join(ROOT, 'tools', 'with_venv.sh') PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') + def die(message, *args): print >>sys.stderr, message % args sys.exit(1) @@ -97,21 +98,11 @@ def install_dependencies(venv=VENV): f.write("%s\n" % ROOT) -def install_django_nova(path): +def install_django_nova(): print 'Installing django_nova in development mode...' + path = os.path.join(ROOT, '..', 'django-nova') run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=path) -def print_usage(): - usage = """ - OpenStack Dashboard development uses virtualenv to track and manage Python - dependencies while in development and testing. - - It uses the OpenStack django_nova module. For more information on how to - obtian the django_nova module, please refer to the README file. - - usage: python install_venv.py <path to django_nova checkout> - """ - print usage def print_summary(): summary = """ @@ -125,15 +116,12 @@ def print_summary(): print summary -def main(argv): - if len(argv) != 2: - print_usage() - sys.exit(1) +def main(): check_dependencies() create_virtualenv() install_dependencies() - install_django_nova(argv[1]) + install_django_nova() print_summary() if __name__ == '__main__': - main(sys.argv) + main() diff --git a/tools/pip-requires b/openstack-dashboard/tools/pip-requires index 608447d1..608447d1 100644 --- a/tools/pip-requires +++ b/openstack-dashboard/tools/pip-requires diff --git a/tools/with_venv.sh b/openstack-dashboard/tools/with_venv.sh index 91299647..91299647 100755 --- a/tools/with_venv.sh +++ b/openstack-dashboard/tools/with_venv.sh |