diff --git a/.gitignore b/.gitignore index 1450cea90..dc4986a96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,16 @@ *.pyc +*.swp +django-openstack/.coverage django-openstack/.installed.cfg django-openstack/bin django-openstack/develop-eggs/ django-openstack/downloads/ django-openstack/eggs/ +django-openstack/htmlcov +django-openstack/launchpad django-openstack/parts/ -django-openstack/src/django_nova.egg-info -django-openstack/src/django_openstack.egg-info +django-openstack/django_nova.egg-info +django-openstack/django_openstack.egg-info django-nova-syspanel/src/django_nova_syspanel.egg-info openstack-dashboard/.dashboard-venv openstack-dashboard/local/dashboard_openstack.sqlite3 diff --git a/django-openstack/README b/django-openstack/README index cc89b6cfb..5c0efa213 100644 --- a/django-openstack/README +++ b/django-openstack/README @@ -22,7 +22,13 @@ Getting Started --------------- Django-Nova uses Buildout (http://www.buildout.org/) to manage local -development. To configure your local Buildout environment: +development. To configure your local Buildout environment first install the following +system-level dependencies: + * python-dev + * git + * bzr + +Then instantiate buildout with $ python bootstrap.py $ bin/buildout @@ -37,6 +43,14 @@ scripts in the bin/ directory: You should now be able to run unit tests as follows: $ bin/django test +or + $ bin/test +You can run unit tests with code coverage on django_openstack by setting +NOSE_WITH_COVERAGE: + $ NOSE_WITH_COVERAGE=true bin/test +Get even better coverage info by running coverage directly: + + $ coverage run --branch --source django_openstack bin/django test django_openstack && coverage html diff --git a/django-openstack/buildout.cfg b/django-openstack/buildout.cfg index 32e915b99..1c66bfc29 100644 --- a/django-openstack/buildout.cfg +++ b/django-openstack/buildout.cfg @@ -1,22 +1,108 @@ [buildout] -parts = python django +parts = + django + launchpad + openstack-compute + openstackx develop = . -eggs = django-openstack versions = versions + [versions] django = 1.3 +# the following are for glance-dependencies +eventlet = 0.9.12 +greenlet = 0.3.1 +pep8 = 0.5.0 +sqlalchemy = 0.6.3 +sqlalchemy-migrate = 0.6 +webob = 1.0.8 -[python] + +[dependencies] +# dependencies that are found locally ${buildout:directory}/module +# or can be fetched from pypi recipe = zc.recipe.egg +eggs = + django-mailer + httplib2 + python-cloudfiles interpreter = python -eggs = ${buildout:eggs} + + +# glance doesn't have a client, and installing +# from bzr doesn't install deps +[glance-dependencies] +recipe = zc.recipe.egg +eggs = + PasteDeploy + anyjson + argparse + eventlet + greenlet + paste + pep8 + routes + sqlalchemy + sqlalchemy-migrate + webob +interpreter = python + + +[django-openstack] +recipe = zc.recipe.egg +eggs = django-openstack +interpreter = python + [django] +# defines settings for django +# any dependencies that cannot be satisifed via the dependencies +# recipe above will need to be added to the extra-paths here. +# IE, dependencies fetch from a git repo will not auto-populate +# like the zc.recipe.egg ones will recipe = djangorecipe project = django_openstack projectegg = django_openstack -settings = tests.testsettings +settings = tests test = django_openstack -eggs = ${buildout:eggs} +eggs = + ${dependencies:eggs} + ${django-openstack:eggs} + ${glance-dependencies:eggs} +extra-paths = + ${buildout:directory}/launchpad/glance + ${buildout:directory}/parts/openstack-compute + ${buildout:directory}/parts/openstackx + +## Dependencies fetch from git +# git dependencies end up as a subdirectory of ${buildout:directory}/parts/ +[openstack-compute] +recipe = zerokspot.recipe.git +repository = git://github.com/jacobian/openstack.compute.git +as_egg = True + +[openstackx] +recipe = zerokspot.recipe.git +repository = git://github.com/cloudbuilders/openstackx.git +as_egg = True + + +## Dependencies fetched from launchpad +# launchpad dependencies will appear as subfolders of +# ${buildout:directory}/launchpad/ +# multiple urls can be specified, format is +# branch_url subfolder_name +# don't forget to add directory to extra_paths in [django] +[launchpad] +recipe = bazaarrecipe +urls = + https://launchpad.net/~hudson-openstack/glance/trunk/ glance + + +## Dependencies fetch from other bzr locations +#[bzrdeps] +#recipe = bazaarrecipe +#urls = +# https://launchpad.net/~hudson-openstack/glance/trunk/ glance diff --git a/django-openstack/django_openstack/api.py b/django-openstack/django_openstack/api.py index b9384eff1..a8460951a 100644 --- a/django-openstack/django_openstack/api.py +++ b/django-openstack/django_openstack/api.py @@ -1,93 +1,344 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +""" +Methods and interface objects used to interact with external apis. + +API method calls return objects that are in many cases objects with +attributes that are direct maps to the data returned from the API http call. +Unfortunately, these objects are also often constructed dynamically, making +it difficult to know what data is available from the API object. Because of +this, all API calls should wrap their returned object in one defined here, +using only explicitly defined atributes and/or methods. + +In other words, django_openstack developers not working on django_openstack.api +shouldn't need to understand the finer details of APIs for Nova/Glance/Swift et +al. +""" from django.conf import settings -import logging - +import cloudfiles import glance.client import httplib import json +import logging import openstack.compute import openstackx.admin +import openstackx.api.exceptions as api_exceptions import openstackx.extras import openstackx.auth from urlparse import urlparse -import json + + +LOG = logging.getLogger('django_openstack.api') + + +class APIResourceWrapper(object): + """ Simple wrapper for api objects + + Define _attrs on the child class and pass in the + api object as the only argument to the constructor + """ + _attrs = [] + + def __init__(self, apiresource): + self._apiresource = apiresource + + def __getattr__(self, attr): + if attr in self._attrs: + # __getattr__ won't find properties + return self._apiresource.__getattribute__(attr) + else: + LOG.debug('Attempted to access unknown attribute "%s" on' + ' APIResource object of type "%s" wrapping resource of' + ' type "%s"' % (attr, self.__class__, + self._apiresource.__class__)) + raise AttributeError(attr) + + +class APIDictWrapper(object): + """ Simple wrapper for api dictionaries + + Some api calls return dictionaries. This class provides identical + behavior as APIResourceWrapper, except that it will also behave as a + dictionary, in addition to attribute accesses. + + Attribute access is the preferred method of access, to be + consistent with api resource objects from openstackx + """ + def __init__(self, apidict): + self._apidict = apidict + + def __getattr__(self, attr): + if attr in self._attrs: + try: + return self._apidict[attr] + except KeyError, e: + raise AttributeError(e) + + else: + LOG.debug('Attempted to access unknown item "%s" on' + 'APIResource object of type "%s"' + % (attr, self.__class__)) + raise AttributeError(attr) + + def __getitem__(self, item): + try: + return self.__getattr__(item) + except AttributeError, e: + # caller is expecting a KeyError + raise KeyError(e) + + def get(self, item, default=None): + try: + return self.__getattr__(item) + except AttributeError: + return default + + +class Container(APIResourceWrapper): + """Simple wrapper around cloudfiles.container.Container""" + _attrs = ['name'] + + +class Console(APIResourceWrapper): + """Simple wrapper around openstackx.extras.consoles.Console""" + _attrs = ['id', 'output', 'type'] + + +class Flavor(APIResourceWrapper): + """Simple wrapper around openstackx.admin.flavors.Flavor""" + _attrs = ['disk', 'id', 'links', 'name', 'ram', 'vcpus'] + + +class Image(APIDictWrapper): + """Simple wrapper around glance image dictionary""" + _attrs = ['checksum', 'container_format', 'created_at', 'deleted', + 'deleted_at', 'disk_format', 'id', 'is_public', 'location', + 'name', 'properties', 'size', 'status', 'updated_at'] + + def __getattr__(self, attrname): + if attrname == "properties": + return ImageProperties(super(Image, self).__getattr__(attrname)) + else: + return super(Image, self).__getattr__(attrname) + + +class ImageProperties(APIDictWrapper): + """Simple wrapper around glance image properties dictionary""" + _attrs = ['architecture', 'image_location', 'image_state', 'kernel_id', + 'project_id', 'ramdisk_id'] + + +class KeyPair(APIResourceWrapper): + """Simple wrapper around openstackx.extras.keypairs.Keypair""" + _attrs = ['fingerprint', 'key_name', 'private_key'] + + +class Server(APIResourceWrapper): + """Simple wrapper around openstackx.extras.server.Server + + Preserves the request info so image name can later be retrieved + """ + _attrs = ['addresses', 'attrs', 'hostId', 'id', 'imageRef', 'links', + 'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid', + 'image_name'] + + def __init__(self, apiresource, request): + super(Server, self).__init__(apiresource) + self.request = request + + def __getattr__(self, attr): + if attr == "attrs": + return ServerAttributes(super(Server, self).__getattr__(attr)) + else: + return super(Server, self).__getattr__(attr) + + @property + def image_name(self): + image = image_get(self.request, self.imageRef) + return image.name + + +class ServerAttributes(APIDictWrapper): + """Simple wrapper around openstackx.extras.server.Server attributes + + Preserves the request info so image name can later be retrieved + """ + _attrs = ['description', 'disk_gb', 'host', 'image_ref', 'kernel_id', + 'key_name', 'launched_at', 'mac_address', 'memory_mb', 'name', + 'os_type', 'project_id', 'ramdisk_id', 'scheduled_at', + 'terminated_at', 'user_data', 'user_id', 'vcpus', 'hostname'] + + +class Services(APIResourceWrapper): + _attrs = ['disabled', 'host', 'id', 'last_update', 'stats', 'type', 'up', + 'zone'] + + +class SwiftObject(APIResourceWrapper): + _attrs = ['name'] + + +class Tenant(APIResourceWrapper): + """Simple wrapper around openstackx.auth.tokens.Tenant""" + _attrs = ['id', 'description', 'enabled'] + + +class Token(APIResourceWrapper): + """Simple wrapper around openstackx.auth.tokens.Token""" + _attrs = ['id', 'serviceCatalog', 'tenant_id', 'username'] + + +class Usage(APIResourceWrapper): + """Simple wrapper around openstackx.extras.usage.Usage""" + _attrs = ['begin', 'instances', 'stop', 'tenant_id', + 'total_active_disk_size', 'total_active_instances', + 'total_active_ram_size', 'total_active_vcpus', 'total_cpu_usage', + 'total_disk_usage', 'total_hours', 'total_ram_usage'] + + +class User(APIResourceWrapper): + """Simple wrapper around openstackx.extras.users.User""" + _attrs = ['email', 'enabled', 'id', 'tenantId'] + def url_for(request, service_name, admin=False): catalog = request.session['serviceCatalog'] if admin: - rv = catalog[service_name][0]['adminURL'] + rv = catalog[service_name][0]['adminURL'] else: rv = catalog[service_name][0]['internalURL'] return rv + +def check_openstackx(f): + """Decorator that adds extra info to api exceptions + + The dashboard currently depends on openstackx extensions being present + in nova. Error messages depending for views depending on these + extensions do not lead to the conclusion that nova is missing + extensions. + + This decorator should be dropped and removed after keystone and + dashboard more gracefully handle extensions and openstackx extensions + aren't required by the dashboard in nova. + """ + def inner(*args, **kwargs): + try: + return f(*args, **kwargs) + except api_exceptions.NotFound, e: + e.message = e.details or '' + e.message += ' This error may be caused by missing openstackx' \ + ' extensions in nova. See the dashboard README.' + raise + + return inner + + def compute_api(request): - compute = openstack.compute.Compute(auth_token=request.session['token'], - management_url=url_for(request, 'nova')) + compute = openstack.compute.Compute( + auth_token=request.session['token'], + management_url=url_for(request, 'nova')) # this below hack is necessary to make the jacobian compute client work + # TODO(mgius): It looks like this is unused now? compute.client.auth_token = request.session['token'] compute.client.management_url = url_for(request, 'nova') + LOG.debug('compute_api connection created using token "%s"' + ' and url "%s"' % + (request.session['token'], url_for(request, 'nova'))) return compute + def account_api(request): - return openstackx.extras.Account(auth_token=request.session['token'], - management_url=url_for(request, 'keystone', True)) + LOG.debug('account_api connection created using token "%s"' + ' and url "%s"' % + (request.session['token'], + url_for(request, 'identity', True))) + return openstackx.extras.Account( + auth_token=request.session['token'], + management_url=url_for(request, 'identity', True)) + def glance_api(request): o = urlparse(url_for(request, 'glance')) + LOG.debug('glance_api connection created for host "%s:%d"' % + (o.hostname, o.port)) return glance.client.Client(o.hostname, o.port) + def admin_api(request): + LOG.debug('admin_api connection created using token "%s"' + ' and url "%s"' % + (request.session['token'], url_for(request, 'nova', True))) return openstackx.admin.Admin(auth_token=request.session['token'], management_url=url_for(request, 'nova', True)) + def extras_api(request): + LOG.debug('extras_api connection created using token "%s"' + ' and url "%s"' % + (request.session['token'], url_for(request, 'nova'))) return openstackx.extras.Extras(auth_token=request.session['token'], management_url=url_for(request, 'nova')) def auth_api(): - return openstackx.auth.Auth(management_url=\ - settings.OPENSTACK_KEYSTONE_URL) + LOG.debug('auth_api connection created using url "%s"' % + settings.OPENSTACK_KEYSTONE_URL) + return openstackx.auth.Auth( + management_url=settings.OPENSTACK_KEYSTONE_URL) + + +def swift_api(): + return cloudfiles.get_connection( + settings.SWIFT_ACCOUNT + ":" + settings.SWIFT_USER, + settings.SWIFT_PASS, + authurl=settings.SWIFT_AUTHURL) def console_create(request, instance_id, kind='text'): - return extras_api(request).consoles.create(instance_id, kind) + return Console(extras_api(request).consoles.create(instance_id, kind)) def flavor_create(request, name, memory, vcpu, disk, flavor_id): - return admin_api(request).flavors.create( - name, int(memory), int(vcpu), int(disk), flavor_id) + return Flavor(admin_api(request).flavors.create( + name, int(memory), int(vcpu), int(disk), flavor_id)) def flavor_delete(request, flavor_id, purge=False): - return admin_api(request).flavors.delete(flavor_id, purge) + admin_api(request).flavors.delete(flavor_id, purge) def flavor_get(request, flavor_id): - return compute_api(request).flavors.get(flavor_id) + return Flavor(compute_api(request).flavors.get(flavor_id)) +@check_openstackx def flavor_list(request): - return extras_api(request).flavors.list() - - -def flavor_list_admin(request): - return extras_api(request).flavors.list() - - -def image_all_metadata(request): - images = glance_api(request).get_images_detailed() - image_dict = {} - for image in images: - image_dict[image['id']] = image - return image_dict + return [Flavor(f) for f in extras_api(request).flavors.list()] def image_create(request, image_meta, image_file): - return glance_api(request).add_image(image_meta, image_file) + return Image(glance_api(request).add_image(image_meta, image_file)) def image_delete(request, image_id): @@ -95,52 +346,56 @@ def image_delete(request, image_id): def image_get(request, image_id): - return glance_api(request).get_image(image_id)[0] + return Image(glance_api(request).get_image(image_id)[0]) def image_list_detailed(request): - return glance_api(request).get_images_detailed() + return [Image(i) for i in glance_api(request).get_images_detailed()] def image_update(request, image_id, image_meta=None): image_meta = image_meta and image_meta or {} - return glance_api(request).update_image(image_id, image_meta=image_meta) + return Image(glance_api(request).update_image(image_id, + image_meta=image_meta)) def keypair_create(request, name): - return extras_api(request).keypairs.create(name) + return KeyPair(extras_api(request).keypairs.create(name)) def keypair_delete(request, keypair_id): - return extras_api(request).keypairs.delete(keypair_id) + extras_api(request).keypairs.delete(keypair_id) +@check_openstackx def keypair_list(request): - return extras_api(request).keypairs.list() + return [KeyPair(key) for key in extras_api(request).keypairs.list()] -def server_create(request, name, image, flavor, - key_name, user_data, security_groups): - return extras_api(request).servers.create( - name, image, flavor, None, None, None, - key_name, user_data, security_groups) +def server_create(request, name, image, flavor, user_data, key_name): + return Server(extras_api(request).servers.create( + name, image, flavor, user_data=user_data, key_name=key_name), + request) def server_delete(request, instance): - return compute_api(request).servers.delete(instance) + compute_api(request).servers.delete(instance) def server_get(request, instance_id): - return extras_api(request).servers.get(instance_id) + return Server(compute_api(request).servers.get(instance_id), request) +@check_openstackx def server_list(request): - return extras_api(request).servers.list() + return [Server(s, request) for s in extras_api(request).servers.list()] -def server_reboot(request, instance_id, hardness=openstack.compute.servers.REBOOT_HARD): +def server_reboot(request, + instance_id, + hardness=openstack.compute.servers.REBOOT_HARD): server = server_get(request, instance_id) - return server.reboot(hardness) + server.reboot(hardness) def server_update(request, instance_id, name, description): @@ -150,56 +405,68 @@ def server_update(request, instance_id, name, description): def service_get(request, name): - return admin_api(request).services.get(name) + return Services(admin_api(request).services.get(name)) +@check_openstackx def service_list(request): - return admin_api(request).services.list() + return [Services(s) for s in admin_api(request).services.list()] def service_update(request, name, enabled): - return admin_api(request).services.update(name, enabled) + return Services(admin_api(request).services.update(name, enabled)) def token_get_tenant(request, tenant_id): tenants = auth_api().tenants.for_token(request.session['token']) for t in tenants: if str(t.id) == str(tenant_id): - return t + return Tenant(t) + + LOG.warning('Unknown tenant id "%s" requested' % tenant_id) def token_list_tenants(request, token): - return auth_api().tenants.for_token(token) + return [Tenant(t) for t in auth_api().tenants.for_token(token)] def tenant_create(request, tenant_id, description, enabled): - return account_api(request).tenants.create(tenant_id, description, enabled) + return Tenant(account_api(request).tenants.create(tenant_id, + description, + enabled)) def tenant_get(request, tenant_id): - return account_api(request).tenants.get(tenant_id) + return Tenant(account_api(request).tenants.get(tenant_id)) +@check_openstackx def tenant_list(request): - return account_api(request).tenants.list() + return [Tenant(t) for t in account_api(request).tenants.list()] def tenant_update(request, tenant_id, description, enabled): - return account_api(request).tenants.update(tenant_id, description, enabled) + return Tenant(account_api(request).tenants.update(tenant_id, + description, + enabled)) def token_create(request, tenant, username, password): - return auth_api().tokens.create(tenant, username, password) + return Token(auth_api().tokens.create(tenant, username, password)) def tenant_quota_get(request, tenant): return admin_api(request).quota_sets.get(tenant) def token_info(request, token): + # TODO(mgius): This function doesn't make a whole lot of sense to me. The + # information being gathered here really aught to be attached to Token() as + # part of token_create. May require modification of openstackx so that the + # token_create call returns this information as well hdrs = {"Content-type": "application/json", "X_AUTH_TOKEN": settings.OPENSTACK_ADMIN_TOKEN, "Accept": "text/json"} - o = urlparse(token.serviceCatalog['keystone'][0]['adminURL']) + o = urlparse(token.serviceCatalog['identity'][0]['adminURL']) conn = httplib.HTTPConnection(o.hostname, o.port) conn.request("GET", "/v2.0/tokens/%s" % token.id, headers=hdrs) response = conn.getresponse() @@ -215,38 +482,108 @@ def token_info(request, token): 'admin': admin} +@check_openstackx def usage_get(request, tenant_id, start, end): - return extras_api(request).usage.get(tenant_id, start, end) + return Usage(extras_api(request).usage.get(tenant_id, start, end)) +@check_openstackx def usage_list(request, start, end): - return extras_api(request).usage.list(start, end) + return [Usage(u) for u in extras_api(request).usage.list(start, end)] def user_create(request, user_id, email, password, tenant_id, enabled): - return account_api(request).users.create( - user_id, email, password, tenant_id, enabled) + return User(account_api(request).users.create( + user_id, email, password, tenant_id, enabled)) def user_delete(request, user_id): - return account_api(request).users.delete(user_id) + account_api(request).users.delete(user_id) def user_get(request, user_id): - return account_api(request).users.get(user_id) + return User(account_api(request).users.get(user_id)) +@check_openstackx def user_list(request): - return account_api(request).users.list() + return [User(u) for u in account_api(request).users.list()] def user_update_email(request, user_id, email): - return account_api(request).users.update_email(user_id, email) + return User(account_api(request).users.update_email(user_id, email)) def user_update_password(request, user_id, password): - return account_api(request).users.update_password(user_id, password) + return User(account_api(request).users.update_password(user_id, password)) def user_update_tenant(request, user_id, tenant_id): - return account_api(request).users.update_tenant(user_id, tenant_id) + return User(account_api(request).users.update_tenant(user_id, tenant_id)) + + +def swift_container_exists(container_name): + try: + swift_api().get_container(container_name) + return True + except cloudfiles.errors.NoSuchContainer: + return False + + +def swift_object_exists(container_name, object_name): + container = swift_api().get_container(container_name) + + try: + container.get_object(object_name) + return True + except cloudfiles.errors.NoSuchObject: + return False + + +def swift_get_containers(): + return [Container(c) for c in swift_api().get_all_containers()] + + +def swift_create_container(name): + if swift_container_exists(name): + raise Exception('Container with name %s already exists.' % (name)) + + return Container(swift_api().create_container(name)) + + +def swift_delete_container(name): + swift_api().delete_container(name) + + +def swift_get_objects(container_name, prefix=None): + container = swift_api().get_container(container_name) + return [SwiftObject(o) for o in container.get_objects(prefix=prefix)] + + +def swift_copy_object(orig_container_name, orig_object_name, + new_container_name, new_object_name): + + container = swift_api().get_container(orig_container_name) + + if swift_object_exists(new_container_name, new_object_name) == True: + raise Exception('Object with name %s already exists in container %s' + % (new_object_name, new_container_name)) + + orig_obj = container.get_object(orig_object_name) + return orig_obj.copy_to(new_container_name, new_object_name) + + +def swift_upload_object(container_name, object_name, object_data): + container = swift_api().get_container(container_name) + obj = container.create_object(object_name) + obj.write(object_data) + + +def swift_delete_object(container_name, object_name): + container = swift_api().get_container(container_name) + container.delete_object(object_name) + + +def swift_get_object_data(container_name, object_name): + container = swift_api().get_container(container_name) + return container.get_object(object_name).stream() diff --git a/django-openstack/django_openstack/auth/urls.py b/django-openstack/django_openstack/auth/urls.py index 36d44a319..bc8d45eb7 100644 --- a/django-openstack/django_openstack/auth/urls.py +++ b/django-openstack/django_openstack/auth/urls.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.conf.urls.defaults import * from django.conf import settings diff --git a/django-openstack/django_openstack/auth/views.py b/django-openstack/django_openstack/auth/views.py index b41fbe4b3..f7417ceec 100644 --- a/django-openstack/django_openstack/auth/views.py +++ b/django-openstack/django_openstack/auth/views.py @@ -1,8 +1,25 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import logging -from django import http from django import template from django import shortcuts from django.contrib import messages @@ -12,10 +29,13 @@ from django_openstack import forms from openstackx.api import exceptions as api_exceptions +LOG = logging.getLogger('django_openstack.auth') + + class Login(forms.SelfHandlingForm): username = forms.CharField(max_length="20", label="User Name") password = forms.CharField(max_length="20", label="Password", - widget=forms.PasswordInput()) + widget=forms.PasswordInput(render_value=False)) def handle(self, request, data): try: @@ -24,18 +44,21 @@ class Login(forms.SelfHandlingForm): data['username'], data['password']) info = api.token_info(request, token) - + request.session['token'] = token.id request.session['user'] = info['user'] request.session['tenant'] = data.get('tenant', info['tenant']) request.session['admin'] = info['admin'] request.session['serviceCatalog'] = token.serviceCatalog - logging.info(token.serviceCatalog) + LOG.info('Login form for user "%s". Service Catalog data:\n%s' % + (data['username'], token.serviceCatalog)) return shortcuts.redirect('dash_overview') except api_exceptions.Unauthorized as e: - messages.error(request, 'Error authenticating: %s' % e.message) + msg = 'Error authenticating: %s' % e.message + LOG.error(msg, exc_info=True) + messages.error(request, msg) except api_exceptions.ApiException as e: messages.error(request, 'Error authenticating with keystone: %s' % e.message) @@ -65,7 +88,8 @@ def login(request): def switch_tenants(request, tenant_id): form, handled = LoginWithTenant.maybe_handle( - request, initial={'tenant': tenant_id, 'username': request.user.username}) + request, initial={'tenant': tenant_id, + 'username': request.user.username}) if handled: return handled diff --git a/django-openstack/django_openstack/context_processors.py b/django-openstack/django_openstack/context_processors.py index fce33b60b..2eda1b884 100644 --- a/django-openstack/django_openstack/context_processors.py +++ b/django-openstack/django_openstack/context_processors.py @@ -1,3 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +from django.conf import settings from django_openstack import api from django.contrib import messages from openstackx.api import exceptions as api_exceptions @@ -13,3 +34,7 @@ def tenants(request): messages.error(request, "Unable to retrieve tenant list from\ keystone: %s" % e.message) return {'tenants': []} + + +def swift(request): + return {'swift_configured': hasattr(settings, "SWIFT_AUTHURL")} diff --git a/django-openstack/django_openstack/dash/urls.py b/django-openstack/django_openstack/dash/urls.py index 75be611a6..7ea5a0fca 100644 --- a/django-openstack/django_openstack/dash/urls.py +++ b/django-openstack/django_openstack/dash/urls.py @@ -1,11 +1,30 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.conf.urls.defaults import * -from django.conf import settings INSTANCES = r'^(?P[^/]+)/instances/(?P[^/]+)/%s$' IMAGES = r'^(?P[^/]+)/images/(?P[^/]+)/%s$' KEYPAIRS = r'^(?P[^/]+)/keypairs/%s$' +CONTAINERS = r'^(?P[^/]+)/containers/%s$' +OBJECTS = r'^(?P[^/]+)/containers/(?P[^/]+)/%s$' urlpatterns = patterns('django_openstack.dash.views.instances', url(r'^(?P[^/]+)/$', 'usage', name='dash_usage'), @@ -25,3 +44,18 @@ urlpatterns += patterns('django_openstack.dash.views.keypairs', url(r'^(?P[^/]+)/keypairs/$', 'index', name='dash_keypairs'), url(KEYPAIRS % 'create', 'create', name='dash_keypairs_create'), ) + +# Swift containers and objects. +urlpatterns += patterns('django_openstack.dash.views.containers', + url(CONTAINERS % '', 'index', name='dash_containers'), + url(CONTAINERS % 'create', 'create', name='dash_containers_create'), +) + +urlpatterns += patterns('django_openstack.dash.views.objects', + url(OBJECTS % '', 'index', name='dash_objects'), + url(OBJECTS % 'upload', 'upload', name='dash_objects_upload'), + url(OBJECTS % '(?P[^/]+)/copy', + 'copy', name='dash_object_copy'), + url(OBJECTS % '(?P[^/]+)/download', + 'download', name='dash_objects_download'), +) diff --git a/django-openstack/django_openstack/dash/views/containers.py b/django-openstack/django_openstack/dash/views/containers.py new file mode 100644 index 000000000..ac92de3a1 --- /dev/null +++ b/django-openstack/django_openstack/dash/views/containers.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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 Swift containers. +""" +import logging + +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django import shortcuts + +from django_openstack import api +from django_openstack import forms + +from cloudfiles.errors import ContainerNotEmpty + + +LOG = logging.getLogger('django_openstack.dash') + + +class DeleteContainer(forms.SelfHandlingForm): + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + try: + api.swift_delete_container(data['container_name']) + except ContainerNotEmpty, e: + messages.error(request, + 'Unable to delete non-empty container: %s' % \ + data['container_name']) + LOG.error('Unable to delete container "%s". Exception: "%s"' % + (data['container_name'], str(e))) + else: + messages.info(request, + 'Successfully deleted container: %s' % \ + data['container_name']) + return shortcuts.redirect(request.build_absolute_uri()) + + +class CreateContainer(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label="Container Name") + + def handle(self, request, data): + api.swift_create_container(data['name']) + messages.success(request, "Container was successfully created.") + return shortcuts.redirect(request.build_absolute_uri()) + + +@login_required +def index(request, tenant_id): + delete_form, handled = DeleteContainer.maybe_handle(request) + if handled: + return handled + + containers = api.swift_get_containers() + + return shortcuts.render_to_response('dash_containers.html', { + 'containers': containers, + 'delete_form': delete_form, + }, context_instance=template.RequestContext(request)) + + +@login_required +def create(request, tenant_id): + form, handled = CreateContainer.maybe_handle(request) + if handled: + return handled + + return shortcuts.render_to_response('dash_containers_create.html', { + 'create_form': form, + }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index 3a2b958e0..de89ff2d2 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -39,7 +41,7 @@ from openstackx.api import exceptions as api_exceptions from glance.common import exception as glance_exception -LOG = logging.getLogger('django_openstack.dash') +LOG = logging.getLogger('django_openstack.dash.views.images') from django.core import validators import re @@ -59,8 +61,6 @@ class LaunchForm(forms.SelfHandlingForm): required=False) name = forms.CharField(max_length=80, label="Server Name") - security_groups = forms.CharField(max_length=255, validators=[validators.RegexValidator(regex=re.compile(r'^[0-9A-Za-z,\.\-_]*$'))], required=False) - # make the dropdown populate when the form is loaded not when django is # started def __init__(self, *args, **kwargs): @@ -82,7 +82,6 @@ class LaunchForm(forms.SelfHandlingForm): field_list = ( 'name', 'user_data', - 'security_groups', 'flavor', 'key_name') for field in field_list[::-1]: @@ -100,14 +99,16 @@ class LaunchForm(forms.SelfHandlingForm): image, flavor, data.get('key_name'), - data.get('user_data'), - data.get('security_groups').split(',')) + data.get('user_data')) - messages.success(request, "Instance was successfully\ - launched.") + msg = 'Instance was successfully launched' + LOG.info(msg) + messages.success(request, msg) return redirect('dash_instances', tenant_id) except api_exceptions.ApiException, e: + LOG.error('ApiException while creating instances of image "%s"' % + image_id, exc_info=True) messages.error(request, 'Unable to launch instance: %s' % e.message) @@ -115,7 +116,7 @@ class LaunchForm(forms.SelfHandlingForm): @login_required def index(request, tenant_id): tenant = {} - + try: tenant = api.token_get_tenant(request, request.user.tenant) except api_exceptions.ApiException, e: @@ -127,24 +128,15 @@ def index(request, tenant_id): all_images = api.image_list_detailed(request) if not all_images: messages.info(request, "There are currently no images.") - except GlanceClientConnectionError, e: - messages.error(request, "Error connecting to glance: %s" % e.message) + except glance_exception.ClientConnectionError, e: + LOG.error("Error connecting to glance", exc_info=True) + messages.error(request, "Error connecting to glance: %s" % str(e)) except glance_exception.Error, e: - messages.error(request, "Error retrieving image list: %s" % e.message) + LOG.error("Error retrieving image list", exc_info=True) + messages.error(request, "Error retrieving image list: %s" % str(e)) - images = [] - - def convert_time(tstr): - if tstr: - return datetime.datetime.strptime(tstr, "%Y-%m-%dT%H:%M:%S.%f") - else: - return '' - - for im in all_images: - im['created'] = convert_time(im['created_at']) - im['updated'] = convert_time(im['updated_at']) - if im['container_format'] not in ['aki', 'ari']: - images.append(im) + images = [im for im in all_images + if im['container_format'] not in ['aki', 'ari']] return render_to_response('dash_images.html', { 'tenant': tenant, @@ -162,7 +154,9 @@ def launch(request, tenant_id, image_id): sel = [(f.id, '%s (%svcpu / %sGB Disk / %sMB Ram )' % (f.name, f.vcpus, f.disk, f.ram)) for f in fl] return sorted(sel) - except: + except api_exceptions.ApiException: + LOG.error('Unable to retrieve list of instance types', + exc_info=True) return [(1, 'm1.tiny')] def keynamelist(): @@ -170,15 +164,17 @@ def launch(request, tenant_id, image_id): fl = api.keypair_list(request) sel = [(f.key_name, f.key_name) for f in fl] return sel - except: + except api_exceptions.ApiException: + LOG.error('Unable to retrieve list of keypairs', exc_info=True) return [] + # TODO(mgius): Any reason why these can't be after the launchform logic? + # If The form is valid, we've just wasted these two api calls try: image = api.image_get(request, image_id) tenant = api.token_get_tenant(request, request.user.tenant) quotas = api.tenant_quota_get(request, request.user.tenant) quotas.ram = int(quotas.ram)/100 - except Exception, e: messages.error(request, 'Unable to retrieve image %s: %s' % (image_id, e.message)) diff --git a/django-openstack/django_openstack/dash/views/instances.py b/django-openstack/django_openstack/dash/views/instances.py index bda19df95..e87a1b9d7 100644 --- a/django-openstack/django_openstack/dash/views/instances.py +++ b/django-openstack/django_openstack/dash/views/instances.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -23,15 +25,16 @@ import datetime import logging from django import http +from django import shortcuts 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.utils.translation import ugettext as _ from django_openstack import api from django_openstack import forms +from django_openstack import utils import openstack.compute.servers import openstackx.api.exceptions as api_exceptions @@ -49,14 +52,17 @@ class TerminateInstance(forms.SelfHandlingForm): try: api.server_delete(request, instance) except api_exceptions.ApiException, e: + LOG.error('ApiException while terminating instance "%s"' % + instance_id, exc_info=True) messages.error(request, 'Unable to terminate %s: %s' % (instance_id, e.message,)) else: - messages.success(request, - 'Instance %s has been terminated.' % instance_id) + msg = 'Instance %s has been terminated.' % instance_id + LOG.info(msg) + messages.success(request, msg) - return redirect(request.build_absolute_uri()) + return shortcuts.redirect(request.build_absolute_uri()) class RebootInstance(forms.SelfHandlingForm): @@ -68,10 +74,17 @@ class RebootInstance(forms.SelfHandlingForm): server = api.server_reboot(request, instance_id) messages.success(request, "Instance rebooting") except api_exceptions.ApiException, e: + LOG.error('ApiException while rebooting instance "%s"' % + instance_id, exc_info=True) messages.error(request, 'Unable to reboot instance: %s' % e.message) - return redirect(request.build_absolute_uri()) + else: + msg = 'Instance %s has been rebooted.' % instance_id + LOG.info(msg) + messages.success(request, msg) + + return shortcuts.redirect(request.build_absolute_uri()) class UpdateInstance(forms.Form): @@ -89,13 +102,13 @@ def index(request, tenant_id): return handled instances = [] try: - image_dict = api.image_all_metadata(request) instances = api.server_list(request) for instance in instances: # FIXME - ported this over, but it is hacky instance.attrs['image_name'] =\ image_dict.get(int(instance.attrs['image_ref']),{}).get('name') - except Exception as e: + except api_exceptions.ApiException as e: + LOG.error('Exception in instance index', exc_info=True) messages.error(request, 'Unable to get instance list: %s' % e.message) # We don't have any way of showing errors for these, so don't bother @@ -103,7 +116,7 @@ def index(request, tenant_id): terminate_form = TerminateInstance() reboot_form = RebootInstance() - return render_to_response('dash_instances.html', { + return shortcuts.render_to_response('dash_instances.html', { 'instances': instances, 'terminate_form': terminate_form, 'reboot_form': reboot_form, @@ -130,19 +143,19 @@ def refresh(request, tenant_id): # trying to reuse the forms from above terminate_form = TerminateInstance() reboot_form = RebootInstance() - + return render_to_response('_instance_list.html', { 'instances': instances, 'terminate_form': terminate_form, 'reboot_form': reboot_form, }, context_instance=template.RequestContext(request)) - + @login_required def usage(request, tenant_id=None): - today = datetime.date.today() + today = utils.today() date_start = datetime.date(today.year, today.month, 1) - datetime_start = datetime.datetime.combine(date_start, datetime.time()) - datetime_end = datetime.datetime.utcnow() + datetime_start = datetime.datetime.combine(date_start, utils.time()) + datetime_end = utils.utcnow() show_terminated = request.GET.get('show_terminated', False) @@ -153,6 +166,8 @@ def usage(request, tenant_id=None): try: usage = api.usage_get(request, tenant_id, datetime_start, datetime_end) except api_exceptions.ApiException, e: + LOG.error('ApiException in instance usage', exc_info=True) + messages.error(request, 'Unable to get usage info: %s' % e.message) ram_unit = "MB" @@ -168,7 +183,7 @@ def usage(request, tenant_id=None): if hasattr(usage, 'instances'): now = datetime.datetime.now() for i in usage.instances: - # this is just a way to phrase uptime in a way that is compatible + # this is just a way to phrase uptime in a way that is compatible # with the 'timesince' filter. Use of local time intentional i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime']) if i['ended_at']: @@ -180,7 +195,7 @@ def usage(request, tenant_id=None): if show_terminated: instances += terminated_instances - return render_to_response('dash_usage.html', { + return shortcuts.render_to_response('dash_usage.html', { 'usage': usage, 'ram_unit': ram_unit, 'total_ram': total_ram, @@ -198,10 +213,13 @@ def console(request, tenant_id, instance_id): response.flush() return response except api_exceptions.ApiException, e: + LOG.error('ApiException while fetching instance console', + exc_info=True) + messages.error(request, 'Unable to get log for instance %s: %s' % (instance_id, e.message)) - return redirect('dash_instances', tenant_id) + return shortcuts.redirect('dash_instances', tenant_id) @login_required @@ -209,17 +227,20 @@ def vnc(request, tenant_id, instance_id): try: console = api.console_create(request, instance_id, 'vnc') instance = api.server_get(request, instance_id) - return redirect(console.output + ("&title=%s(%s)" % (instance.name, instance_id))) + return shortcuts.redirect(console.output + ("&title=%s(%s)" % (instance.name, instance_id))) except api_exceptions.ApiException, e: + LOG.error('ApiException while fetching instance vnc connection', + exc_info=True) + messages.error(request, 'Unable to get vnc console for instance %s: %s' % (instance_id, e.message)) - return redirect('dash_instances', tenant_id) + return shortcuts.redirect('dash_instances', tenant_id) @login_required def update(request, tenant_id, instance_id): - if request.POST: + if request.POST: form = UpdateInstance(request.POST) if form.is_valid(): data = form.clean() diff --git a/django-openstack/django_openstack/dash/views/keypairs.py b/django-openstack/django_openstack/dash/views/keypairs.py index b9dd85bf2..4666e3402 100644 --- a/django-openstack/django_openstack/dash/views/keypairs.py +++ b/django-openstack/django_openstack/dash/views/keypairs.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -19,7 +21,6 @@ """ Views for managing Nova instances. """ -import datetime import logging from django import http @@ -34,11 +35,10 @@ from django.utils.translation import ugettext as _ from django_openstack import api from django_openstack import forms -import openstack.compute.servers import openstackx.api.exceptions as api_exceptions -LOG = logging.getLogger('django_openstack.dash') +LOG = logging.getLogger('django_openstack.dash.views.keypairs') class DeleteKeypair(forms.SelfHandlingForm): @@ -46,18 +46,22 @@ class DeleteKeypair(forms.SelfHandlingForm): def handle(self, request, data): try: + LOG.info('Deleting keypair "%s"' % data['keypair_id']) keypair = api.keypair_delete(request, data['keypair_id']) messages.info(request, 'Successfully deleted keypair: %s' \ % data['keypair_id']) except api_exceptions.ApiException, e: + LOG.error("ApiException in DeleteKeypair", exc_info=True) messages.error(request, 'Error deleting keypair: %s' % e.message) return shortcuts.redirect(request.build_absolute_uri()) + class CreateKeypair(forms.SelfHandlingForm): name = forms.CharField(max_length="20", label="Keypair Name") def handle(self, request, data): try: + LOG.info('Creating keypair "%s"' % data['name']) keypair = api.keypair_create(request, slugify(data['name'])) response = http.HttpResponse(mimetype='application/binary') response['Content-Disposition'] = \ @@ -66,9 +70,11 @@ class CreateKeypair(forms.SelfHandlingForm): response.write(keypair.private_key) return response except api_exceptions.ApiException, e: + LOG.error("ApiException in CreateKeyPair", exc_info=True) messages.error(request, 'Error Creating Keypair: %s' % e.message) return shortcuts.redirect(request.build_absolute_uri()) + @login_required def index(request, tenant_id): delete_form, handled = DeleteKeypair.maybe_handle(request) @@ -77,19 +83,21 @@ def index(request, tenant_id): keypairs = api.keypair_list(request) except api_exceptions.ApiException, e: keypairs = [] - messages.error(request, 'Error featching keypairs: %s' % e.message) + LOG.error("ApiException in keypair index", exc_info=True) + messages.error(request, 'Error fetching keypairs: %s' % e.message) - return render_to_response('dash_keypairs.html', { + return shortcuts.render_to_response('dash_keypairs.html', { 'keypairs': keypairs, 'delete_form': delete_form, }, context_instance=template.RequestContext(request)) + @login_required def create(request, tenant_id): form, handled = CreateKeypair.maybe_handle(request) if handled: return handled - return render_to_response('dash_keypairs_create.html', { + return shortcuts.render_to_response('dash_keypairs_create.html', { 'create_form': form, }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/dash/views/objects.py b/django-openstack/django_openstack/dash/views/objects.py new file mode 100644 index 000000000..e73ac84bf --- /dev/null +++ b/django-openstack/django_openstack/dash/views/objects.py @@ -0,0 +1,176 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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 Swift containers. +""" +import logging + +from django import http +from django import template +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django import shortcuts +from django.shortcuts import render_to_response + +from django_openstack import api +from django_openstack import forms + + +LOG = logging.getLogger('django_openstack.dash') + + +class FilterObjects(forms.SelfHandlingForm): + container_name = forms.CharField(widget=forms.HiddenInput()) + object_prefix = forms.CharField(required=False) + + def handle(self, request, data): + object_prefix = data['object_prefix'] or None + + objects = api.swift_get_objects(data['container_name'], + prefix=object_prefix) + + return objects + + +class DeleteObject(forms.SelfHandlingForm): + object_name = forms.CharField(widget=forms.HiddenInput()) + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + api.swift_delete_object( + data['container_name'], + data['object_name']) + messages.info(request, + 'Successfully deleted object: %s' % \ + data['object_name']) + return shortcuts.redirect(request.build_absolute_uri()) + + +class UploadObject(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label="Object Name") + object_file = forms.FileField(label="File") + container_name = forms.CharField(widget=forms.HiddenInput()) + + def handle(self, request, data): + api.swift_upload_object( + data['container_name'], + data['name'], + self.files['object_file'].read()) + + messages.success(request, "Object was successfully uploaded.") + return shortcuts.redirect(request.build_absolute_uri()) + + +class CopyObject(forms.SelfHandlingForm): + new_container_name = forms.ChoiceField( + label="Container to store object in") + + new_object_name = forms.CharField(max_length="255", + label="New object name") + orig_container_name = forms.CharField(widget=forms.HiddenInput()) + orig_object_name = forms.CharField(widget=forms.HiddenInput()) + + def __init__(self, *args, **kwargs): + super(CopyObject, self).__init__(*args, **kwargs) + + container_choices = \ + [(c.name, c.name) for c in api.swift_get_containers()] + self.fields['new_container_name'].choices = container_choices + + def handle(self, request, data): + orig_container_name = data['orig_container_name'] + orig_object_name = data['orig_object_name'] + new_container_name = data['new_container_name'] + new_object_name = data['new_object_name'] + + api.swift_copy_object(orig_container_name, orig_object_name, + new_container_name, new_object_name) + + messages.success(request, + 'Object was successfully copied to %s\%s' % + (new_container_name, new_object_name)) + + return shortcuts.redirect(request.build_absolute_uri()) + + +@login_required +def index(request, tenant_id, container_name): + delete_form, handled = DeleteObject.maybe_handle(request) + if handled: + return handled + + filter_form, objects = FilterObjects.maybe_handle(request) + if not objects: + filter_form.fields['container_name'].initial = container_name + objects = api.swift_get_objects(container_name) + + delete_form.fields['container_name'].initial = container_name + return render_to_response('dash_objects.html', { + 'container_name': container_name, + 'objects': objects, + 'delete_form': delete_form, + 'filter_form': filter_form, + }, context_instance=template.RequestContext(request)) + + +@login_required +def upload(request, tenant_id, container_name): + form, handled = UploadObject.maybe_handle(request) + if handled: + return handled + + form.fields['container_name'].initial = container_name + return render_to_response('dash_objects_upload.html', { + 'container_name': container_name, + 'upload_form': form, + }, context_instance=template.RequestContext(request)) + + +@login_required +def download(request, tenant_id, container_name, object_name): + object_data = api.swift_get_object_data( + container_name, object_name) + + response = http.HttpResponse() + response['Content-Disposition'] = 'attachment; filename=%s' % \ + object_name + for data in object_data: + response.write(data) + return response + + +@login_required +def copy(request, tenant_id, container_name, object_name): + form, handled = CopyObject.maybe_handle(request) + + if handled: + return handled + + form.fields['new_container_name'].initial = container_name + form.fields['orig_container_name'].initial = container_name + form.fields['orig_object_name'].initial = object_name + + return render_to_response( + 'dash_object_copy.html', + {'container_name': container_name, + 'object_name': object_name, + 'copy_form': form}, + context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/forms.py b/django-openstack/django_openstack/forms.py index dfeba1fe6..9195a689b 100644 --- a/django-openstack/django_openstack/forms.py +++ b/django-openstack/django_openstack/forms.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import datetime import logging import re @@ -14,7 +32,7 @@ from django.utils import formats from django.forms import * - +LOG = logging.getLogger('django_openstack.forms') RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') @@ -132,14 +150,16 @@ class SelfHandlingForm(Form): kwargs['initial'] = initial super(SelfHandlingForm, self).__init__(*args, **kwargs) - @classmethod def maybe_handle(cls, request, *args, **kwargs): if cls.__name__ != request.POST.get('method'): return cls(*args, **kwargs), None try: - form = cls(request.POST, *args, **kwargs) + if request.FILES: + form = cls(request.POST, request.FILES, *args, **kwargs) + else: + form = cls(request.POST, *args, **kwargs) if not form.is_valid(): return form, None @@ -148,7 +168,7 @@ class SelfHandlingForm(Form): return form, form.handle(request, data) except Exception as e: - logging.exception('Error while handling form.') + LOG.error('Nonspecific error while handling form', exc_info=True) messages.error(request, 'Unexpected error: %s' % e.message) return form, None diff --git a/django-openstack/django_openstack/middleware/keystone.py b/django-openstack/django_openstack/middleware/keystone.py index e5ada7126..11fec1b4f 100644 --- a/django-openstack/django_openstack/middleware/keystone.py +++ b/django-openstack/django_openstack/middleware/keystone.py @@ -1,4 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.contrib import messages from django import shortcuts import openstackx diff --git a/django-openstack/django_openstack/models.py b/django-openstack/django_openstack/models.py index 5dd1017b1..1ae367d73 100644 --- a/django-openstack/django_openstack/models.py +++ b/django-openstack/django_openstack/models.py @@ -1,3 +1,23 @@ -''' +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +""" Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 -''' +""" diff --git a/django-openstack/django_openstack/syspanel/forms.py b/django-openstack/django_openstack/syspanel/forms.py index d57488e0c..63856ca66 100644 --- a/django-openstack/django_openstack/syspanel/forms.py +++ b/django-openstack/django_openstack/syspanel/forms.py @@ -1,3 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django import forms diff --git a/django-openstack/django_openstack/syspanel/urls.py b/django-openstack/django_openstack/syspanel/urls.py index c4db03270..a34dc7411 100644 --- a/django-openstack/django_openstack/syspanel/urls.py +++ b/django-openstack/django_openstack/syspanel/urls.py @@ -1,3 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.conf.urls.defaults import * from django.conf import settings diff --git a/django-openstack/django_openstack/syspanel/views/flavors.py b/django-openstack/django_openstack/syspanel/views/flavors.py index e61929347..025f39259 100644 --- a/django-openstack/django_openstack/syspanel/views/flavors.py +++ b/django-openstack/django_openstack/syspanel/views/flavors.py @@ -1,5 +1,25 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +import logging + from operator import itemgetter from django import template @@ -15,6 +35,8 @@ from openstackx.api import exceptions as api_exceptions from django_openstack import api from django_openstack import forms +LOG = logging.getLogger('django_openstack.syspanel.views.flavors') + class CreateFlavor(forms.SelfHandlingForm): flavorid = forms.CharField(max_length="10", label="Flavor ID") @@ -30,8 +52,9 @@ class CreateFlavor(forms.SelfHandlingForm): int(data['vcpus']), int(data['disk_gb']), int(data['flavorid'])) - messages.success(request, - '%s was successfully added to flavors.' % data['name']) + msg = '%s was successfully added to flavors.' % data['name'] + LOG.info(msg) + messages.success(request, msg) return redirect('syspanel_flavors') @@ -42,6 +65,7 @@ class DeleteFlavor(forms.SelfHandlingForm): try: flavor_id = data['flavorid'] flavor = api.flavor_get(request, flavor_id) + LOG.info('Deleting flavor with id "%s"' % flavor_id) api.flavor_delete(request, flavor_id, False) messages.info(request, 'Successfully deleted flavor: %s' % flavor.name) @@ -61,8 +85,9 @@ def index(request): flavors = [] try: - flavors = api.flavor_list_admin(request) + flavors = api.flavor_list(request) except api_exceptions.ApiException, e: + LOG.error('ApiException while fetching usage info', exc_info=True) messages.error(request, 'Unable to get usage info: %s' % e.message) flavors.sort(key=lambda x: x.id, reverse=True) diff --git a/django-openstack/django_openstack/syspanel/views/images.py b/django-openstack/django_openstack/syspanel/views/images.py index 4a8cedb84..f79f8d003 100644 --- a/django-openstack/django_openstack/syspanel/views/images.py +++ b/django-openstack/django_openstack/syspanel/views/images.py @@ -1,18 +1,40 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +import logging + from django import template -from django import http -from django.conf import settings from django.contrib import messages from django.shortcuts import redirect from django.shortcuts import render_to_response from django.contrib.auth.decorators import login_required + from glance.common import exception as glance_exception from django_openstack import api from django_openstack import forms +LOG = logging.getLogger('django_openstack.sysadmin.views.images') + + class DeleteImage(forms.SelfHandlingForm): image_id = forms.CharField(required=True) @@ -21,8 +43,12 @@ class DeleteImage(forms.SelfHandlingForm): try: api.image_delete(request, image_id) except glance_exception.ClientConnectionError, e: - messages.error(request, "Error connecting to glance: %s" % e.message) + LOG.error("Error connecting to glance", exc_info=True) + messages.error(request, + "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: + LOG.error('Error deleting image with id "%s"' % image_id, + exc_info=True) messages.error(request, "Error deleting image: %s" % e.message) return redirect(request.build_absolute_uri()) @@ -35,8 +61,12 @@ class ToggleImage(forms.SelfHandlingForm): try: api.image_update(request, image_id, image_meta={'is_public': False}) except glance_exception.ClientConnectionError, e: - messages.error(request, "Error connecting to glance: %s" % e.message) + LOG.error("Error connecting to glance", exc_info=True) + messages.error(request, + "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: + LOG.error('Error updating image with id "%s"' % image_id, + exc_info=True) messages.error(request, "Error updating image: %s" % e.message) return redirect(request.build_absolute_uri()) @@ -50,7 +80,6 @@ class UpdateImageForm(forms.Form): disk_format = forms.CharField(label="Disk Format") #is_public = forms.BooleanField(label="Publicly Available", required=False) - @login_required def index(request): for f in (DeleteImage, ToggleImage): @@ -69,8 +98,10 @@ def index(request): if not images: messages.info(request, "There are currently no images.") except glance_exception.ClientConnectionError, e: + LOG.error("Error connecting to glance", exc_info=True) messages.error(request, "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: + LOG.error("Error retrieving image list", exc_info=True) messages.error(request, "Error retrieving image list: %s" % e.message) return render_to_response('syspanel_images.html', { @@ -85,9 +116,13 @@ def update(request, image_id): try: image = api.image_get(request, image_id) except glance_exception.ClientConnectionError, e: + LOG.error("Error connecting to glance", exc_info=True) messages.error(request, "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: - messages.error(request, "Error retrieving image %s: %s" % (image_id, e.message)) + LOG.error('Error retrieving image with id "%s"' % image_id, + exc_info=True) + messages.error(request, + "Error retrieving image %s: %s" % (image_id, e.message)) if request.method == "POST": form = UpdateImageForm(request.POST) @@ -111,12 +146,24 @@ def update(request, image_id): api.image_update(request, image_id, metadata) messages.success(request, "Image was successfully updated.") except glance_exception.ClientConnectionError, e: - messages.error(request, "Error connecting to glance: %s" % e.message) + LOG.error("Error connecting to glance", exc_info=True) + messages.error(request, + "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: + LOG.error('Error updating image with id "%s"' % image_id, + exc_info=True) messages.error(request, "Error updating image: %s" % e.message) - return redirect("syspanel_images") + except: + LOG.error('Unspecified Exception in image update', + exc_info=True) + messages.error(request, + "Image could not be updated, please try again.") + else: - messages.error(request, "Image could not be updated, please try agian.") + LOG.error('Image "%s" failed to update' % image['name'], + exc_info=True) + messages.error(request, + "Image could not be uploaded, please try agian.") form = UpdateImageForm(request.POST) return render_to_response('syspanel_image_update.html',{ 'image': image, @@ -141,3 +188,47 @@ def update(request, image_id): 'form': form, }, context_instance = template.RequestContext(request)) + +@login_required +def upload(request): + if request.method == "POST": + form = UploadImageForm(request.POST) + if form.is_valid(): + image = form.clean() + metadata = {'is_public': image['is_public'], + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': image['name']} + try: + messages.success(request, "Image was successfully uploaded.") + except: + # TODO add better error management + messages.error(request, "Image could not be uploaded, please try again.") + + try: + api.image_create(request, metadata, image['image_file']) + except glance_exception.ClientConnectionError, e: + LOG.error('Error connecting to glance while trying to upload' + ' image', exc_info=True) + messages.error(request, + "Error connecting to glance: %s" % e.message) + except glance_exception.Error, e: + LOG.error('Glance exception while uploading image', + exc_info=True) + messages.error(request, "Error adding image: %s" % e.message) + else: + LOG.error('Image "%s" failed to upload' % image['name'], + exc_info=True) + messages.error(request, + "Image could not be uploaded, please try agian.") + form = UploadImageForm(request.POST) + return render_to_response('django_nova_syspanel/images/image_upload.html',{ + 'form': form, + }, context_instance = template.RequestContext(request)) + + return redirect('syspanel_images') + else: + form = UploadImageForm() + return render_to_response('django_nova_syspanel/images/image_upload.html',{ + 'form': form, + }, context_instance = template.RequestContext(request)) diff --git a/django-openstack/django_openstack/syspanel/views/instances.py b/django-openstack/django_openstack/syspanel/views/instances.py index 3dd6d056f..8b479c237 100644 --- a/django-openstack/django_openstack/syspanel/views/instances.py +++ b/django-openstack/django_openstack/syspanel/views/instances.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django import template from django import http from django.conf import settings @@ -21,6 +39,8 @@ from openstackx.api import exceptions as api_exceptions TerminateInstance = dash_instances.TerminateInstance RebootInstance = dash_instances.RebootInstance +LOG = logging.getLogger('django_openstack.syspanel.views.instances') + def _next_month(date_start): y = date_start.year + (date_start.month + 1)/13 @@ -67,7 +87,10 @@ def usage(request): try: service_list = api.service_list(request) except api_exceptions.ApiException, e: - messages.error(request, 'Unable to get service info: %s' % e.message) + LOG.error('ApiException fetching service list in instance usage', + exc_info=True) + messages.error(request, + 'Unable to get service info: %s' % e.message) for service in service_list: if service.type == 'nova-compute': @@ -78,6 +101,10 @@ def usage(request): try: usage_list = api.usage_list(request, datetime_start, datetime_end) except api_exceptions.ApiException, e: + LOG.error('ApiException fetching usage list in instance usage' + ' on date range "%s to %s"' % (datetime_start, + datetime_end), + exc_info=True) messages.error(request, 'Unable to get usage info: %s' % e.message) dateform = forms.DateForm() @@ -88,9 +115,13 @@ def usage(request): 'total_active_ram_size': 0} for usage in usage_list: - usage = usage.to_dict() - for k in usage: - v = usage[k] + # FIXME: api needs a simpler dict interface (with iteration) - anthony + # NOTE(mgius): Changed this on the api end. Not too much neater, but + # at least its not going into private member data of an external + # class anymore + #usage = usage._info + for k in usage._attrs: + v = usage.__getattr__(k) if type(v) in [float, int]: if not k in global_summary: global_summary[k] = 0 @@ -145,6 +176,10 @@ def tenant_usage(request, tenant_id): try: usage = api.usage_get(request, tenant_id, datetime_start, datetime_end) except api_exceptions.ApiException, e: + LOG.error('ApiException getting usage info for tenant "%s"' + ' on date range "%s to %s"' % (tenant_id, + datetime_start, + datetime_end)) messages.error(request, 'Unable to get usage info: %s' % e.message) running_instances = [] @@ -152,7 +187,7 @@ def tenant_usage(request, tenant_id): if hasattr(usage, 'instances'): now = datetime.datetime.now() for i in usage.instances: - # this is just a way to phrase uptime in a way that is compatible + # this is just a way to phrase uptime in a way that is compatible # with the 'timesince' filter. Use of local time intentional i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime']) if i['ended_at']: @@ -177,13 +212,13 @@ def index(request): instances = [] try: - image_dict = api.image_all_metadata(request) instances = api.server_list(request) for instance in instances: # FIXME - ported this over, but it is hacky instance._info['attrs']['image_name'] =\ image_dict.get(int(instance.attrs['image_ref']),{}).get('name') except Exception as e: + LOG.error('Unspecified error in instance index', exc_info=True) messages.error(request, 'Unable to get instance list: %s' % e.message) # We don't have any way of showing errors for these, so don't bother diff --git a/django-openstack/django_openstack/syspanel/views/services.py b/django-openstack/django_openstack/syspanel/views/services.py index b65c0af18..5841d2d46 100644 --- a/django-openstack/django_openstack/syspanel/views/services.py +++ b/django-openstack/django_openstack/syspanel/views/services.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django import template from django import http from django.conf import settings @@ -12,6 +30,7 @@ import datetime import json import logging import subprocess +import sys import urlparse from django.contrib import messages @@ -21,6 +40,8 @@ from django_openstack import forms from django_openstack.dash.views import instances as dash_instances from openstackx.api import exceptions as api_exceptions +LOG = logging.getLogger('django_openstack.syspanel.views.services') + class ToggleService(forms.SelfHandlingForm): service = forms.CharField(required=False) @@ -39,6 +60,8 @@ class ToggleService(forms.SelfHandlingForm): messages.info(request, "Service '%s' has been disabled" % data['name']) except api_exceptions.ApiException, e: + LOG.error('ApiException while toggling service %s' % + data['service'], exc_info=True) messages.error(request, "Unable to update service '%s': %s" % data['name'], e.message) @@ -56,6 +79,7 @@ def index(request): try: services = api.service_list(request) except api_exceptions.ApiException, e: + LOG.error('ApiException fetching service list', exc_info=True) messages.error(request, 'Unable to get service info: %s' % e.message) other_services = [] @@ -63,7 +87,11 @@ def index(request): for k, v in request.session['serviceCatalog'].iteritems(): v = v[0] try: - subprocess.check_call(['curl', '-m', '1', v['internalURL']]) + # TODO(mgius): This silences curl, but there's probably + # a better solution than using curl to begin with + subprocess.check_call(['curl', '-m', '1', v['internalURL']], + stdout=open(sys.devnull, 'w'), + stderr=open(sys.devnull, 'w')) up = True except: up = False diff --git a/django-openstack/django_openstack/syspanel/views/tenants.py b/django-openstack/django_openstack/syspanel/views/tenants.py index 4f161074b..2b9322c13 100644 --- a/django-openstack/django_openstack/syspanel/views/tenants.py +++ b/django-openstack/django_openstack/syspanel/views/tenants.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django import template from django import http from django.conf import settings @@ -20,12 +38,15 @@ from django_openstack.dash.views import instances as dash_instances from openstackx.api import exceptions as api_exceptions +LOG = logging.getLogger('django_openstack.syspanel.views.tenants') + + class AddUser(forms.SelfHandlingForm): user = forms.CharField() tenant = forms.CharField() - + def handle(self, request, data): - try: + try: api.account_api(request).role_refs.add_for_tenant_user(data['tenant'], data['user'], settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE) messages.success(request, @@ -40,9 +61,9 @@ class AddUser(forms.SelfHandlingForm): class RemoveUser(forms.SelfHandlingForm): user = forms.CharField() tenant = forms.CharField() - + def handle(self, request, data): - try: + try: api.account_api(request).role_refs.delete_for_tenant_user(data['tenant'], data['user'], 'Member') messages.success(request, @@ -61,6 +82,7 @@ class CreateTenant(forms.SelfHandlingForm): def handle(self, request, data): try: + LOG.info('Creating tenant with id "%s"' % data['id']) api.tenant_create(request, data['id'], data['description'], @@ -69,6 +91,10 @@ class CreateTenant(forms.SelfHandlingForm): '%s was successfully created.' % data['id']) except api_exceptions.ApiException, e: + LOG.error('ApiException while creating tenant\n' + 'Id: "%s", Description: "%s", Enabled "%s"' % + (data['id'], data['description'], data['enabled']), + exc_info=True) messages.error(request, 'Unable to create tenant: %s' % (e.message)) return redirect('syspanel_tenants') @@ -81,6 +107,7 @@ class UpdateTenant(forms.SelfHandlingForm): def handle(self, request, data): try: + LOG.info('Updating tenant with id "%s"' % data['id']) api.tenant_update(request, data['id'], data['description'], @@ -89,9 +116,14 @@ class UpdateTenant(forms.SelfHandlingForm): '%s was successfully updated.' % data['id']) except api_exceptions.ApiException, e: + LOG.error('ApiException while updating tenant\n' + 'Id: "%s", Description: "%s", Enabled "%s"' % + (data['id'], data['description'], data['enabled']), + exc_info=True) messages.error(request, 'Unable to update tenant: %s' % e.message) return redirect('syspanel_tenants') + class UpdateQuotas(forms.SelfHandlingForm): tenant_id = forms.CharField(label="ID (name)", widget=forms.TextInput(attrs={'readonly':'readonly'})) metadata_items = forms.CharField(label="Metadata Items") @@ -125,12 +157,14 @@ class UpdateQuotas(forms.SelfHandlingForm): messages.error(request, 'Unable to update quotas: %s' % e.message) return redirect('syspanel_tenants') + @login_required def index(request): tenants = [] try: tenants = api.tenant_list(request) except api_exceptions.ApiException, e: + LOG.error('ApiException while getting tenant list', exc_info=True) messages.error(request, 'Unable to get tenant info: %s' % e.message) tenants.sort(key=lambda x: x.id, reverse=True) return render_to_response('syspanel_tenants.html',{ @@ -163,6 +197,8 @@ def update(request, tenant_id): 'description': tenant.description, 'enabled': tenant.enabled}) except api_exceptions.ApiException, e: + LOG.error('Error fetching tenant with id "%s"' % tenant_id, + exc_info=True) messages.error(request, 'Unable to update tenant: %s' % e.message) return redirect('syspanel_tenants') @@ -204,6 +240,7 @@ def users(request, tenant_id): 'new_users': new_user_ids, }, context_instance = template.RequestContext(request)) + @login_required def quotas(request, tenant_id): for f in (UpdateQuotas,): diff --git a/django-openstack/django_openstack/syspanel/views/users.py b/django-openstack/django_openstack/syspanel/views/users.py index f2e618f14..5b6ea62c9 100644 --- a/django-openstack/django_openstack/syspanel/views/users.py +++ b/django-openstack/django_openstack/syspanel/views/users.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django import template from django import http from django.conf import settings @@ -20,6 +38,8 @@ from django_openstack.dash.views import instances as dash_instances from openstackx.api import exceptions as api_exceptions +LOG = logging.getLogger('django_openstack.syspanel.views.users') + class UserForm(forms.Form): def __init__(self, *args, **kwargs): @@ -38,6 +58,7 @@ class UserDeleteForm(forms.SelfHandlingForm): def handle(self, request, data): user_id = data['user'] + LOG.info('Deleting user with id "%s"' % user_id) api.user_delete(request, user_id) messages.info(request, '%s was successfully deleted.' % user_id) @@ -137,6 +158,7 @@ def create(request): user = form.clean() # TODO Make this a real request try: + LOG.info('Creating user with id "%s"' % user['id']) api.user_create(request, user['id'], user['email'], @@ -153,6 +175,10 @@ def create(request): return redirect('syspanel_users') except api_exceptions.ApiException, e: + LOG.error('ApiException while creating user\n' + 'id: "%s", email: "%s", tenant_id: "%s"' % + (user['id'], user['email'], user['tenant_id']), + exc_info=True) messages.error(request, 'Error creating user: %s' % e.message) diff --git a/django-openstack/django_openstack/templatetags/templatetags/branding.py b/django-openstack/django_openstack/templatetags/templatetags/branding.py index fa421adb2..ce7d458ed 100644 --- a/django-openstack/django_openstack/templatetags/templatetags/branding.py +++ b/django-openstack/django_openstack/templatetags/templatetags/branding.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 diff --git a/django-openstack/django_openstack/templatetags/templatetags/parse_date.py b/django-openstack/django_openstack/templatetags/templatetags/parse_date.py index 2c98190ed..70dbb3227 100644 --- a/django-openstack/django_openstack/templatetags/templatetags/parse_date.py +++ b/django-openstack/django_openstack/templatetags/templatetags/parse_date.py @@ -1,3 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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 parsing date strings. """ diff --git a/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py b/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py index 675904fc1..9c24893f9 100644 --- a/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py +++ b/django-openstack/django_openstack/templatetags/templatetags/truncate_filter.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -15,6 +17,7 @@ # 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. """ diff --git a/django-openstack/django_openstack/tests/__init__.py b/django-openstack/django_openstack/tests/__init__.py index e69de29bb..137e26173 100644 --- a/django-openstack/django_openstack/tests/__init__.py +++ b/django-openstack/django_openstack/tests/__init__.py @@ -0,0 +1 @@ +from testsettings import * diff --git a/django-openstack/django_openstack/tests/api_tests.py b/django-openstack/django_openstack/tests/api_tests.py new file mode 100644 index 000000000..460c975b8 --- /dev/null +++ b/django-openstack/django_openstack/tests/api_tests.py @@ -0,0 +1,1541 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +import cloudfiles +import httplib +import json +import mox + +from django import http +from django import test +from django.conf import settings +from django_openstack import api +from glance import client as glance_client +from mox import IsA +from openstack import compute as OSCompute +from openstackx import admin as OSAdmin +from openstackx import auth as OSAuth +from openstackx import extras as OSExtras + + +TEST_CONSOLE_KIND = 'vnc' +TEST_EMAIL = 'test@test.com' +TEST_HOSTNAME = 'hostname' +TEST_INSTANCE_ID = '2' +TEST_PASSWORD = '12345' +TEST_PORT = 8000 +TEST_RETURN = 'retValue' +TEST_TENANT_DESCRIPTION = 'tenantDescription' +TEST_TENANT_ID = '1234' +TEST_TOKEN = 'aToken' +TEST_TOKEN_ID = 'userId' +TEST_URL = 'http://%s:%s/something/v1.0' % (TEST_HOSTNAME, TEST_PORT) +TEST_USERNAME = 'testUser' + + +class Server(object): + """ More or less fakes what the api is looking for """ + def __init__(self, id, imageRef, attrs=None): + self.id = id + self.imageRef = imageRef + if attrs is not None: + self.attrs = attrs + + def __eq__(self, other): + if self.id != other.id or \ + self.imageRef != other.imageRef: + return False + + for k in self.attrs: + if other.attrs.__getattr__(k) != v: + return False + + return True + + def __ne__(self, other): + return not self == other + + +class Tenant(object): + """ More or less fakes what the api is looking for """ + def __init__(self, id, description, enabled): + self.id = id + self.description = description + self.enabled = enabled + + def __eq__(self, other): + return self.id == other.id and \ + self.description == other.description and \ + self.enabled == other.enabled + + def __ne__(self, other): + return not self == other + + +class Token(object): + """ More or less fakes what the api is looking for """ + def __init__(self, id, username, tenant_id, serviceCatalog=None): + self.id = id + self.username = username + self.tenant_id = tenant_id + self.serviceCatalog = serviceCatalog + + def __eq__(self, other): + return self.id == other.id and \ + self.username == other.username and \ + self.tenant_id == other.tenant_id and \ + self.serviceCatalog == other.serviceCatalog + + def __ne__(self, other): + return not self == other + + +class APIResource(api.APIResourceWrapper): + """ Simple APIResource for testing """ + _attrs = ['foo', 'bar', 'baz'] + + @staticmethod + def get_instance(innerObject=None): + if innerObject is None: + class InnerAPIResource(object): + pass + innerObject = InnerAPIResource() + innerObject.foo = 'foo' + innerObject.bar = 'bar' + return APIResource(innerObject) + + +class APIDict(api.APIDictWrapper): + """ Simple APIDict for testing """ + _attrs = ['foo', 'bar', 'baz'] + + @staticmethod + def get_instance(innerDict=None): + if innerDict is None: + innerDict = {'foo': 'foo', + 'bar': 'bar'} + return APIDict(innerDict) + + +class APIResourceWrapperTests(test.TestCase): + def test_get_attribute(self): + resource = APIResource.get_instance() + self.assertEqual(resource.foo, 'foo') + + def test_get_invalid_attribute(self): + resource = APIResource.get_instance() + self.assertNotIn('missing', resource._attrs, + msg="Test assumption broken. Find new missing attribute") + with self.assertRaises(AttributeError): + resource.missing + + def test_get_inner_missing_attribute(self): + resource = APIResource.get_instance() + with self.assertRaises(AttributeError): + resource.baz + + +class APIDictWrapperTests(test.TestCase): + # APIDict allows for both attribute access and dictionary style [element] + # style access. Test both + def test_get_item(self): + resource = APIDict.get_instance() + self.assertEqual(resource.foo, 'foo') + self.assertEqual(resource['foo'], 'foo') + + def test_get_invalid_item(self): + resource = APIDict.get_instance() + self.assertNotIn('missing', resource._attrs, + msg="Test assumption broken. Find new missing attribute") + with self.assertRaises(AttributeError): + resource.missing + with self.assertRaises(KeyError): + resource['missing'] + + def test_get_inner_missing_attribute(self): + resource = APIDict.get_instance() + with self.assertRaises(AttributeError): + resource.baz + with self.assertRaises(KeyError): + resource['baz'] + + def test_get_with_default(self): + resource = APIDict.get_instance() + + self.assertEqual(resource.get('foo'), 'foo') + + self.assertIsNone(resource.get('baz')) + + self.assertEqual('retValue', resource.get('baz', 'retValue')) + + +# Wrapper classes that only define _attrs don't need extra testing. +# Wrapper classes that have other attributes or methods need testing +class ImageWrapperTests(test.TestCase): + dict_with_properties = { + 'properties': + {'image_state': 'running'}, + 'size': 100, + } + dict_without_properties = { + 'size': 100, + } + + def test_get_properties(self): + image = api.Image(self.dict_with_properties) + image_props = image.properties + self.assertIsInstance(image_props, api.ImageProperties) + self.assertEqual(image_props.image_state, 'running') + + def test_get_other(self): + image = api.Image(self.dict_with_properties) + self.assertEqual(image.size, 100) + + def test_get_properties_missing(self): + image = api.Image(self.dict_without_properties) + with self.assertRaises(AttributeError): + image.properties + + def test_get_other_missing(self): + image = api.Image(self.dict_without_properties) + with self.assertRaises(AttributeError): + self.assertNotIn('missing', image._attrs, + msg="Test assumption broken. Find new missing attribute") + image.missing + + +class ServerWrapperTests(test.TestCase): + HOST = 'hostname' + ID = '1' + IMAGE_NAME = 'imageName' + IMAGE_REF = '3' + + def setUp(self): + self.mox = mox.Mox() + + # these are all objects "fetched" from the api + self.inner_attrs = {'host': self.HOST} + + self.inner_server = Server(self.ID, self.IMAGE_REF, self.inner_attrs) + self.inner_server_no_attrs = Server(self.ID, self.IMAGE_REF) + + self.request = self.mox.CreateMock(http.HttpRequest) + + def tearDown(self): + self.mox.UnsetStubs() + + def test_get_attrs(self): + server = api.Server(self.inner_server, self.request) + attrs = server.attrs + # for every attribute in the "inner" object passed to the api wrapper, + # see if it can be accessed through the api.ServerAttribute instance + for k in self.inner_attrs: + self.assertEqual(attrs.__getattr__(k), self.inner_attrs[k]) + + def test_get_other(self): + server = api.Server(self.inner_server, self.request) + self.assertEqual(server.id, self.ID) + + def test_get_attrs_missing(self): + server = api.Server(self.inner_server_no_attrs, self.request) + with self.assertRaises(AttributeError): + server.attrs + + def test_get_other_missing(self): + server = api.Server(self.inner_server, self.request) + with self.assertRaises(AttributeError): + self.assertNotIn('missing', server._attrs, + msg="Test assumption broken. Find new missing attribute") + server.missing + + def test_image_name(self): + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + self.IMAGE_REF + ).AndReturn(api.Image({'name': self.IMAGE_NAME})) + + server = api.Server(self.inner_server, self.request) + + self.mox.ReplayAll() + + image_name = server.image_name + + self.assertEqual(image_name, self.IMAGE_NAME) + + self.mox.VerifyAll() + + +class ApiHelperTests(test.TestCase): + """ Tests for functions that don't use one of the api objects """ + def setUp(self): + self.mox = mox.Mox() + self.request = http.HttpRequest() + self.request.session = dict() + + def tearDown(self): + self.mox.UnsetStubs() + + def test_url_for(self): + GLANCE_URL = 'http://glance/glanceapi/' + NOVA_URL = 'http://nova/novapi/' + + serviceCatalog = { + 'glance': [{'adminURL': GLANCE_URL + 'admin', + 'internalURL': GLANCE_URL + 'internal'}, + ], + 'nova': [{'adminURL': NOVA_URL + 'admin', + 'internalURL': NOVA_URL + 'internal'}, + ], + } + + self.request.session['serviceCatalog'] = serviceCatalog + + url = api.url_for(self.request, 'glance') + self.assertEqual(url, GLANCE_URL + 'internal') + + url = api.url_for(self.request, 'glance', admin=False) + self.assertEqual(url, GLANCE_URL + 'internal') + + url = api.url_for(self.request, 'glance', admin=True) + self.assertEqual(url, GLANCE_URL + 'admin') + + url = api.url_for(self.request, 'nova') + self.assertEqual(url, NOVA_URL + 'internal') + + url = api.url_for(self.request, 'nova', admin=False) + self.assertEqual(url, NOVA_URL + 'internal') + + url = api.url_for(self.request, 'nova', admin=True) + self.assertEqual(url, NOVA_URL + 'admin') + + def test_token_info(self): + """ This function uses the keystone api, but not through an + api client, because there doesn't appear to be one for + keystone + """ + GLANCE_URL = 'http://glance/glance_api/' + KEYSTONE_HOST = 'keystonehost' + KEYSTONE_PORT = 8080 + KEYSTONE_URL = 'http://%s:%d/keystone/' % (KEYSTONE_HOST, + KEYSTONE_PORT) + + serviceCatalog = { + 'glance': [{'adminURL': GLANCE_URL + 'admin', + 'internalURL': GLANCE_URL + 'internal'}, + ], + 'identity': [{'adminURL': KEYSTONE_URL + 'admin', + 'internalURL': KEYSTONE_URL + 'internal'}, + ], + } + + token = Token(TEST_TOKEN_ID, TEST_TENANT_ID, + TEST_USERNAME, serviceCatalog) + + jsonData = { + 'auth': { + 'token': { + 'expires': '2011-07-02T02:01:19.382655', + 'id': '3c5748d5-bec6-4215-843a-f959d589f4b0', + }, + 'user': { + 'username': 'joeuser', + 'roleRefs': [{'roleId': 'Minion'}], + 'tenantId': u'1234' + } + } + } + + jsonDataAdmin = { + 'auth': { + 'token': { + 'expires': '2011-07-02T02:01:19.382655', + 'id': '3c5748d5-bec6-4215-843a-f959d589f4b0', + }, + 'user': { + 'username': 'joeuser', + 'roleRefs': [{'roleId': 'Admin'}], + 'tenantId': u'1234' + } + } + } + + # setup test where user is not admin + self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') + + conn = httplib.HTTPConnection(KEYSTONE_HOST, KEYSTONE_PORT) + response = self.mox.CreateMock(httplib.HTTPResponse) + + conn.request(IsA(str), IsA(str), headers=IsA(dict)) + conn.getresponse().AndReturn(response) + + response.read().AndReturn(json.dumps(jsonData)) + + expected_nonadmin_val = { + 'tenant': '1234', + 'user': 'joeuser', + 'admin': False + } + + # setup test where user is admin + conn = httplib.HTTPConnection(KEYSTONE_HOST, KEYSTONE_PORT) + response = self.mox.CreateMock(httplib.HTTPResponse) + + conn.request(IsA(str), IsA(str), headers=IsA(dict)) + conn.getresponse().AndReturn(response) + + response.read().AndReturn(json.dumps(jsonDataAdmin)) + + expected_admin_val = { + 'tenant': '1234', + 'user': 'joeuser', + 'admin': True + } + + self.mox.ReplayAll() + + ret_val = api.token_info(None, token) + + self.assertDictEqual(ret_val, expected_nonadmin_val) + + ret_val = api.token_info(None, token) + + self.assertDictEqual(ret_val, expected_admin_val) + + self.mox.VerifyAll() + + +class AccountApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + self.request = http.HttpRequest() + self.request.session = dict() + self.request.session['token'] = TEST_TOKEN + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_account_api(self): + self.mox.StubOutWithMock(api, 'account_api') + account_api = self.mox.CreateMock(OSExtras.Account) + api.account_api(IsA(http.HttpRequest)).AndReturn(account_api) + return account_api + + def test_get_account_api(self): + self.mox.StubOutClassWithMocks(OSExtras, 'Account') + OSExtras.Account(auth_token=TEST_TOKEN, management_url=TEST_URL) + + self.mox.StubOutWithMock(api, 'url_for') + api.url_for( + IsA(http.HttpRequest), 'identity', True).AndReturn(TEST_URL) + api.url_for( + IsA(http.HttpRequest), 'identity', True).AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.account_api(self.request)) + + self.mox.VerifyAll() + + def test_tenant_create(self): + DESCRIPTION = 'aDescription' + ENABLED = True + + account_api = self.stub_account_api() + + account_api.tenants = self.mox.CreateMockAnything() + account_api.tenants.create(TEST_TENANT_ID, DESCRIPTION, + ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_create(self.request, TEST_TENANT_ID, + DESCRIPTION, ENABLED) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_tenant_get(self): + account_api = self.stub_account_api() + + account_api.tenants = self.mox.CreateMockAnything() + account_api.tenants.get(TEST_TENANT_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_get(self.request, TEST_TENANT_ID) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_tenant_list(self): + tenants = (TEST_RETURN, TEST_RETURN + '2') + + account_api = self.stub_account_api() + + account_api.tenants = self.mox.CreateMockAnything() + account_api.tenants.list().AndReturn(tenants) + + self.mox.ReplayAll() + + ret_val = api.tenant_list(self.request) + + self.assertEqual(len(ret_val), len(tenants)) + for tenant in ret_val: + self.assertIsInstance(tenant, api.Tenant) + self.assertIn(tenant._apiresource, tenants) + + self.mox.VerifyAll() + + def test_tenant_update(self): + DESCRIPTION = 'aDescription' + ENABLED = True + + account_api = self.stub_account_api() + + account_api.tenants = self.mox.CreateMockAnything() + account_api.tenants.update(TEST_TENANT_ID, DESCRIPTION, + ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.tenant_update(self.request, TEST_TENANT_ID, + DESCRIPTION, ENABLED) + + self.assertIsInstance(ret_val, api.Tenant) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_create(self): + account_api = self.stub_account_api() + + account_api.users = self.mox.CreateMockAnything() + account_api.users.create(TEST_USERNAME, TEST_EMAIL, TEST_PASSWORD, + TEST_TENANT_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_create(self.request, TEST_USERNAME, TEST_EMAIL, + TEST_PASSWORD, TEST_TENANT_ID) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_delete(self): + account_api = self.stub_account_api() + + account_api.users = self.mox.CreateMockAnything() + account_api.users.delete(TEST_USERNAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_delete(self.request, TEST_USERNAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_user_get(self): + account_api = self.stub_account_api() + + account_api.users = self.mox.CreateMockAnything() + account_api.users.get(TEST_USERNAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_get(self.request, TEST_USERNAME) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_list(self): + users = (TEST_USERNAME, TEST_USERNAME + '2') + + account_api = self.stub_account_api() + account_api.users = self.mox.CreateMockAnything() + account_api.users.list().AndReturn(users) + + self.mox.ReplayAll() + + ret_val = api.user_list(self.request) + + self.assertEqual(len(ret_val), len(users)) + for user in ret_val: + self.assertIsInstance(user, api.User) + self.assertIn(user._apiresource, users) + + self.mox.VerifyAll() + + def test_user_update_email(self): + account_api = self.stub_account_api() + account_api.users = self.mox.CreateMockAnything() + account_api.users.update_email(TEST_USERNAME, + TEST_EMAIL).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_email(self.request, TEST_USERNAME, + TEST_EMAIL) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_update_password(self): + account_api = self.stub_account_api() + account_api.users = self.mox.CreateMockAnything() + account_api.users.update_password(TEST_USERNAME, + TEST_PASSWORD).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_password(self.request, TEST_USERNAME, + TEST_PASSWORD) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_user_update_tenant(self): + account_api = self.stub_account_api() + account_api.users = self.mox.CreateMockAnything() + account_api.users.update_tenant(TEST_USERNAME, + TEST_TENANT_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.user_update_tenant(self.request, TEST_USERNAME, + TEST_TENANT_ID) + + self.assertIsInstance(ret_val, api.User) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class AdminApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + self.request = http.HttpRequest() + self.request.session = dict() + self.request.session['token'] = TEST_TOKEN + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_admin_api(self, count=1): + self.mox.StubOutWithMock(api, 'admin_api') + admin_api = self.mox.CreateMock(OSAdmin.Admin) + for i in range(count): + api.admin_api(IsA(http.HttpRequest)).AndReturn(admin_api) + return admin_api + + def test_get_admin_api(self): + self.mox.StubOutClassWithMocks(OSAdmin, 'Admin') + OSAdmin.Admin(auth_token=TEST_TOKEN, management_url=TEST_URL) + + self.mox.StubOutWithMock(api, 'url_for') + api.url_for(IsA(http.HttpRequest), 'nova', True).AndReturn(TEST_URL) + api.url_for(IsA(http.HttpRequest), 'nova', True).AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.admin_api(self.request)) + + self.mox.VerifyAll() + + def test_flavor_create(self): + FLAVOR_DISK = 1000 + FLAVOR_ID = 6 + FLAVOR_MEMORY = 1024 + FLAVOR_NAME = 'newFlavor' + FLAVOR_VCPU = 2 + + admin_api = self.stub_admin_api() + + admin_api.flavors = self.mox.CreateMockAnything() + admin_api.flavors.create(FLAVOR_NAME, FLAVOR_MEMORY, FLAVOR_VCPU, + FLAVOR_DISK, FLAVOR_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_create(self.request, FLAVOR_NAME, + str(FLAVOR_MEMORY), str(FLAVOR_VCPU), + str(FLAVOR_DISK), FLAVOR_ID) + + self.assertIsInstance(ret_val, api.Flavor) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_flavor_delete(self): + FLAVOR_ID = 6 + + admin_api = self.stub_admin_api(count=2) + + admin_api.flavors = self.mox.CreateMockAnything() + admin_api.flavors.delete(FLAVOR_ID, False).AndReturn(TEST_RETURN) + admin_api.flavors.delete(FLAVOR_ID, True).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_delete(self.request, FLAVOR_ID) + self.assertIsNone(ret_val) + + ret_val = api.flavor_delete(self.request, FLAVOR_ID, purge=True) + self.assertIsNone(ret_val) + + def test_service_get(self): + NAME = 'serviceName' + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.get(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.service_get(self.request, NAME) + + self.assertIsInstance(ret_val, api.Services) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_service_list(self): + services = (TEST_RETURN, TEST_RETURN + '2') + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.list().AndReturn(services) + + self.mox.ReplayAll() + + ret_val = api.service_list(self.request) + + for service in ret_val: + self.assertIsInstance(service, api.Services) + self.assertIn(service._apiresource, services) + + self.mox.VerifyAll() + + def test_service_update(self): + ENABLED = True + NAME = 'serviceName' + + admin_api = self.stub_admin_api() + admin_api.services = self.mox.CreateMockAnything() + admin_api.services.update(NAME, ENABLED).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.service_update(self.request, NAME, ENABLED) + + self.assertIsInstance(ret_val, api.Services) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + +class AuthApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + + def tearDown(self): + self.mox.UnsetStubs() + + def test_get_auth_api(self): + settings.OPENSTACK_KEYSTONE_URL = TEST_URL + self.mox.StubOutClassWithMocks(OSAuth, 'Auth') + OSAuth.Auth(management_url=settings.OPENSTACK_KEYSTONE_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.auth_api()) + + self.mox.VerifyAll() + + def test_token_get_tenant(self): + self.mox.StubOutWithMock(api, 'auth_api') + auth_api_mock = self.mox.CreateMockAnything() + api.auth_api().AndReturn(auth_api_mock) + + tenants_mock = self.mox.CreateMockAnything() + auth_api_mock.tenants = tenants_mock + + tenant_list = [Tenant('notTheDroid', + 'notTheDroid_desc', + False), + Tenant(TEST_TENANT_ID, + TEST_TENANT_DESCRIPTION, + True), + ] + tenants_mock.for_token('aToken').AndReturn(tenant_list) + + request_mock = self.mox.CreateMock(http.HttpRequest) + request_mock.session = {'token': 'aToken'} + + self.mox.ReplayAll() + + ret_val = api.token_get_tenant(request_mock, TEST_TENANT_ID) + self.assertEqual(tenant_list[1], ret_val) + + self.mox.VerifyAll() + + def test_token_get_tenant_no_tenant(self): + self.mox.StubOutWithMock(api, 'auth_api') + auth_api_mock = self.mox.CreateMockAnything() + api.auth_api().AndReturn(auth_api_mock) + + tenants_mock = self.mox.CreateMockAnything() + auth_api_mock.tenants = tenants_mock + + tenant_list = [Tenant('notTheDroid', + 'notTheDroid_desc', + False), + ] + tenants_mock.for_token('aToken').AndReturn(tenant_list) + + request_mock = self.mox.CreateMock(http.HttpRequest) + request_mock.session = {'token': 'aToken'} + + self.mox.ReplayAll() + + ret_val = api.token_get_tenant(request_mock, TEST_TENANT_ID) + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_token_list_tenants(self): + self.mox.StubOutWithMock(api, 'auth_api') + auth_api_mock = self.mox.CreateMockAnything() + api.auth_api().AndReturn(auth_api_mock) + + tenants_mock = self.mox.CreateMockAnything() + auth_api_mock.tenants = tenants_mock + + tenant_list = [Tenant('notTheDroid', + 'notTheDroid_desc', + False), + Tenant(TEST_TENANT_ID, + TEST_TENANT_DESCRIPTION, + True), + ] + tenants_mock.for_token('aToken').AndReturn(tenant_list) + + request_mock = self.mox.CreateMock(http.HttpRequest) + + self.mox.ReplayAll() + + ret_val = api.token_list_tenants(request_mock, 'aToken') + for tenant in ret_val: + self.assertIn(tenant, tenant_list) + + self.mox.VerifyAll() + + def test_token_create(self): + self.mox.StubOutWithMock(api, 'auth_api') + auth_api_mock = self.mox.CreateMockAnything() + api.auth_api().AndReturn(auth_api_mock) + + tokens_mock = self.mox.CreateMockAnything() + auth_api_mock.tokens = tokens_mock + + test_token = Token(TEST_TOKEN_ID, TEST_USERNAME, TEST_TENANT_ID) + + tokens_mock.create(TEST_TENANT_ID, TEST_USERNAME, + TEST_PASSWORD).AndReturn(test_token) + + request_mock = self.mox.CreateMock(http.HttpRequest) + + self.mox.ReplayAll() + + ret_val = api.token_create(request_mock, TEST_TENANT_ID, + TEST_USERNAME, TEST_PASSWORD) + + self.assertEqual(test_token, ret_val) + + self.mox.VerifyAll() + + +class ComputeApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + self.request = http.HttpRequest() + self.request.session = {} + self.request.session['token'] = TEST_TOKEN + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_compute_api(self, count=1): + self.mox.StubOutWithMock(api, 'compute_api') + compute_api = self.mox.CreateMock(OSCompute.Compute) + for i in range(count): + api.compute_api(IsA(http.HttpRequest)).AndReturn(compute_api) + return compute_api + + def test_get_compute_api(self): + class ComputeClient(object): + __slots__ = ['auth_token', 'management_url'] + + self.mox.StubOutClassWithMocks(OSCompute, 'Compute') + compute_api = OSCompute.Compute(auth_token=TEST_TOKEN, + management_url=TEST_URL) + + compute_api.client = ComputeClient() + + self.mox.StubOutWithMock(api, 'url_for') + # called three times? Looks like a good place for optimization + api.url_for(IsA(http.HttpRequest), 'nova').AndReturn(TEST_URL) + api.url_for(IsA(http.HttpRequest), 'nova').AndReturn(TEST_URL) + api.url_for(IsA(http.HttpRequest), 'nova').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + compute_api = api.compute_api(self.request) + + self.assertIsNotNone(compute_api) + self.assertEqual(compute_api.client.auth_token, TEST_TOKEN) + self.assertEqual(compute_api.client.management_url, TEST_URL) + + self.mox.VerifyAll() + + def test_flavor_get(self): + FLAVOR_ID = 6 + + compute_api = self.stub_compute_api() + + compute_api.flavors = self.mox.CreateMockAnything() + compute_api.flavors.get(FLAVOR_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.flavor_get(self.request, FLAVOR_ID) + self.assertIsInstance(ret_val, api.Flavor) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_server_delete(self): + INSTANCE = 'anInstance' + + compute_api = self.stub_compute_api() + + compute_api.servers = self.mox.CreateMockAnything() + compute_api.servers.delete(INSTANCE).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_delete(self.request, INSTANCE) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_server_get(self): + INSTANCE_ID = '2' + + compute_api = self.stub_compute_api() + compute_api.servers = self.mox.CreateMockAnything() + compute_api.servers.get(INSTANCE_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_get(self.request, INSTANCE_ID) + + self.assertIsInstance(ret_val, api.Server) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_server_reboot(self): + INSTANCE_ID = '2' + HARDNESS = 'diamond' + + self.mox.StubOutWithMock(api, 'server_get') + + server = self.mox.CreateMock(OSCompute.Server) + server.reboot(OSCompute.servers.REBOOT_HARD).AndReturn(TEST_RETURN) + api.server_get(IsA(http.HttpRequest), INSTANCE_ID).AndReturn(server) + + server = self.mox.CreateMock(OSCompute.Server) + server.reboot(HARDNESS).AndReturn(TEST_RETURN) + api.server_get(IsA(http.HttpRequest), INSTANCE_ID).AndReturn(server) + + self.mox.ReplayAll() + + ret_val = api.server_reboot(self.request, INSTANCE_ID) + self.assertIsNone(ret_val) + + ret_val = api.server_reboot(self.request, INSTANCE_ID, + hardness=HARDNESS) + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + +class ExtrasApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + self.request = http.HttpRequest() + self.request.session = dict() + self.request.session['token'] = TEST_TOKEN + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_extras_api(self, count=1): + self.mox.StubOutWithMock(api, 'extras_api') + extras_api = self.mox.CreateMock(OSExtras.Extras) + for i in range(count): + api.extras_api(IsA(http.HttpRequest)).AndReturn(extras_api) + return extras_api + + def test_get_extras_api(self): + self.mox.StubOutClassWithMocks(OSExtras, 'Extras') + OSExtras.Extras(auth_token=TEST_TOKEN, management_url=TEST_URL) + + self.mox.StubOutWithMock(api, 'url_for') + api.url_for(IsA(http.HttpRequest), 'nova').AndReturn(TEST_URL) + api.url_for(IsA(http.HttpRequest), 'nova').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.extras_api(self.request)) + + self.mox.VerifyAll() + + def test_console_create(self): + extras_api = self.stub_extras_api(count=2) + extras_api.consoles = self.mox.CreateMockAnything() + extras_api.consoles.create( + TEST_INSTANCE_ID, TEST_CONSOLE_KIND).AndReturn(TEST_RETURN) + extras_api.consoles.create( + TEST_INSTANCE_ID, None).AndReturn(TEST_RETURN + '2') + + self.mox.ReplayAll() + + ret_val = api.console_create(self.request, + TEST_INSTANCE_ID, + TEST_CONSOLE_KIND) + self.assertIsInstance(ret_val, api.Console) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + ret_val = api.console_create(self.request, TEST_INSTANCE_ID) + self.assertIsInstance(ret_val, api.Console) + self.assertEqual(ret_val._apiresource, TEST_RETURN + '2') + + self.mox.VerifyAll() + + def test_flavor_list(self): + flavors = (TEST_RETURN, TEST_RETURN + '2') + extras_api = self.stub_extras_api() + extras_api.flavors = self.mox.CreateMockAnything() + extras_api.flavors.list().AndReturn(flavors) + + self.mox.ReplayAll() + + ret_val = api.flavor_list(self.request) + + self.assertEqual(len(ret_val), len(flavors)) + for flavor in ret_val: + self.assertIsInstance(flavor, api.Flavor) + self.assertIn(flavor._apiresource, flavors) + + self.mox.VerifyAll() + + def test_keypair_create(self): + NAME = '1' + + extras_api = self.stub_extras_api() + extras_api.keypairs = self.mox.CreateMockAnything() + extras_api.keypairs.create(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.keypair_create(self.request, NAME) + self.assertIsInstance(ret_val, api.KeyPair) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_keypair_delete(self): + KEYPAIR_ID = '1' + + extras_api = self.stub_extras_api() + extras_api.keypairs = self.mox.CreateMockAnything() + extras_api.keypairs.delete(KEYPAIR_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.keypair_delete(self.request, KEYPAIR_ID) + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_keypair_list(self): + NAME = 'keypair' + keypairs = (NAME + '1', NAME + '2') + + extras_api = self.stub_extras_api() + extras_api.keypairs = self.mox.CreateMockAnything() + extras_api.keypairs.list().AndReturn(keypairs) + + self.mox.ReplayAll() + + ret_val = api.keypair_list(self.request) + + self.assertEqual(len(ret_val), len(keypairs)) + for keypair in ret_val: + self.assertIsInstance(keypair, api.KeyPair) + self.assertIn(keypair._apiresource, keypairs) + + self.mox.VerifyAll() + + def test_server_create(self): + NAME = 'server' + IMAGE = 'anImage' + FLAVOR = 'cherry' + USER_DATA = {'nuts': 'berries'} + KEY = 'user' + + extras_api = self.stub_extras_api() + extras_api.servers = self.mox.CreateMockAnything() + extras_api.servers.create(NAME, IMAGE, FLAVOR, user_data=USER_DATA, + key_name=KEY).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.server_create(self.request, NAME, IMAGE, FLAVOR, + USER_DATA, KEY) + + self.assertIsInstance(ret_val, api.Server) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_server_list(self): + servers = (TEST_RETURN, TEST_RETURN + '2') + + extras_api = self.stub_extras_api() + + extras_api.servers = self.mox.CreateMockAnything() + extras_api.servers.list().AndReturn(servers) + + self.mox.ReplayAll() + + ret_val = api.server_list(self.request) + + self.assertEqual(len(ret_val), len(servers)) + for server in ret_val: + self.assertIsInstance(server, api.Server) + self.assertIn(server._apiresource, servers) + + self.mox.VerifyAll() + + def test_usage_get(self): + extras_api = self.stub_extras_api() + + extras_api.usage = self.mox.CreateMockAnything() + extras_api.usage.get(TEST_TENANT_ID, 'start', + 'end').AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.usage_get(self.request, TEST_TENANT_ID, 'start', 'end') + + self.assertIsInstance(ret_val, api.Usage) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_usage_list(self): + usages = (TEST_RETURN, TEST_RETURN + '2') + + extras_api = self.stub_extras_api() + + extras_api.usage = self.mox.CreateMockAnything() + extras_api.usage.list('start', 'end').AndReturn(usages) + + self.mox.ReplayAll() + + ret_val = api.usage_list(self.request, 'start', 'end') + + self.assertEqual(len(ret_val), len(usages)) + for usage in ret_val: + self.assertIsInstance(usage, api.Usage) + self.assertIn(usage._apiresource, usages) + + self.mox.VerifyAll() + + +class GlanceApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + + self.request = http.HttpRequest() + self.request.session = dict() + self.request.session['token'] = TEST_TOKEN + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_glance_api(self, count=1): + self.mox.StubOutWithMock(api, 'glance_api') + glance_api = self.mox.CreateMock(glance_client.Client) + for i in range(count): + api.glance_api(IsA(http.HttpRequest)).AndReturn(glance_api) + return glance_api + + def test_get_glance_api(self): + self.mox.StubOutClassWithMocks(glance_client, 'Client') + glance_client.Client(TEST_HOSTNAME, TEST_PORT) + + self.mox.StubOutWithMock(api, 'url_for') + api.url_for(IsA(http.HttpRequest), 'glance').AndReturn(TEST_URL) + + self.mox.ReplayAll() + + self.assertIsNotNone(api.glance_api(self.request)) + + self.mox.VerifyAll() + + def test_image_create(self): + IMAGE_FILE = 'someData' + IMAGE_META = {'metadata': 'foo'} + + glance_api = self.stub_glance_api() + glance_api.add_image(IMAGE_META, IMAGE_FILE).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_create(self.request, IMAGE_META, IMAGE_FILE) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + self.mox.VerifyAll() + + def test_image_delete(self): + IMAGE_ID = '1' + + glance_api = self.stub_glance_api() + glance_api.delete_image(IMAGE_ID).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_delete(self.request, IMAGE_ID) + + self.assertEqual(ret_val, TEST_RETURN) + + self.mox.VerifyAll() + + def test_image_get(self): + IMAGE_ID = '1' + + glance_api = self.stub_glance_api() + glance_api.get_image(IMAGE_ID).AndReturn([TEST_RETURN]) + + self.mox.ReplayAll() + + ret_val = api.image_get(self.request, IMAGE_ID) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + def test_image_list_detailed(self): + images = (TEST_RETURN, TEST_RETURN + '2') + glance_api = self.stub_glance_api() + glance_api.get_images_detailed().AndReturn(images) + + self.mox.ReplayAll() + + ret_val = api.image_list_detailed(self.request) + + self.assertEqual(len(ret_val), len(images)) + for image in ret_val: + self.assertIsInstance(image, api.Image) + self.assertIn(image._apidict, images) + + self.mox.VerifyAll() + + def test_image_update(self): + IMAGE_ID = '1' + IMAGE_META = {'metadata': 'foobar'} + + glance_api = self.stub_glance_api(count=2) + glance_api.update_image(IMAGE_ID, image_meta={}).AndReturn(TEST_RETURN) + glance_api.update_image(IMAGE_ID, + image_meta=IMAGE_META).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.image_update(self.request, IMAGE_ID) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + ret_val = api.image_update(self.request, + IMAGE_ID, + image_meta=IMAGE_META) + + self.assertIsInstance(ret_val, api.Image) + self.assertEqual(ret_val._apidict, TEST_RETURN) + + self.mox.VerifyAll() + + +class SwiftApiTests(test.TestCase): + def setUp(self): + self.mox = mox.Mox() + + def tearDown(self): + self.mox.UnsetStubs() + + def stub_swift_api(self, count=1): + self.mox.StubOutWithMock(api, 'swift_api') + swift_api = self.mox.CreateMock(cloudfiles.connection.Connection) + for i in range(count): + api.swift_api().AndReturn(swift_api) + return swift_api + + def test_get_swift_api(self): + self.mox.StubOutWithMock(cloudfiles, 'get_connection') + + swiftuser = ':'.join((settings.SWIFT_ACCOUNT, settings.SWIFT_USER)) + cloudfiles.get_connection(swiftuser, + settings.SWIFT_PASS, + authurl=settings.SWIFT_AUTHURL + ).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + self.assertEqual(api.swift_api(), TEST_RETURN) + + self.mox.VerifyAll() + + def test_swift_get_containers(self): + containers = (TEST_RETURN, TEST_RETURN + '2') + + swift_api = self.stub_swift_api() + + swift_api.get_all_containers().AndReturn(containers) + + self.mox.ReplayAll() + + ret_val = api.swift_get_containers() + + self.assertEqual(len(ret_val), len(containers)) + for container in ret_val: + self.assertIsInstance(container, api.Container) + self.assertIn(container._apiresource, containers) + + self.mox.VerifyAll() + + def test_swift_create_container(self): + NAME = 'containerName' + + swift_api = self.stub_swift_api() + self.mox.StubOutWithMock(api, 'swift_container_exists') + + api.swift_container_exists(NAME).AndReturn(False) + swift_api.create_container(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_create_container(NAME) + + self.assertIsInstance(ret_val, api.Container) + self.assertEqual(ret_val._apiresource, TEST_RETURN) + + self.mox.VerifyAll() + + def test_swift_delete_container(self): + NAME = 'containerName' + + swift_api = self.stub_swift_api() + + swift_api.delete_container(NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_delete_container(NAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_get_objects(self): + NAME = 'containerName' + + swift_objects = (TEST_RETURN, TEST_RETURN + '2') + container = self.mox.CreateMock(cloudfiles.container.Container) + container.get_objects(prefix=None).AndReturn(swift_objects) + + swift_api = self.stub_swift_api() + + swift_api.get_container(NAME).AndReturn(container) + + self.mox.ReplayAll() + + ret_val = api.swift_get_objects(NAME) + + self.assertEqual(len(ret_val), len(swift_objects)) + for swift_object in ret_val: + self.assertIsInstance(swift_object, api.SwiftObject) + self.assertIn(swift_object._apiresource, swift_objects) + + self.mox.VerifyAll() + + def test_swift_get_objects_with_prefix(self): + NAME = 'containerName' + PREFIX = 'prefacedWith' + + swift_objects = (TEST_RETURN, TEST_RETURN + '2') + container = self.mox.CreateMock(cloudfiles.container.Container) + container.get_objects(prefix=PREFIX).AndReturn(swift_objects) + + swift_api = self.stub_swift_api() + + swift_api.get_container(NAME).AndReturn(container) + + self.mox.ReplayAll() + + ret_val = api.swift_get_objects(NAME, prefix=PREFIX) + + self.assertEqual(len(ret_val), len(swift_objects)) + for swift_object in ret_val: + self.assertIsInstance(swift_object, api.SwiftObject) + self.assertIn(swift_object._apiresource, swift_objects) + + self.mox.VerifyAll() + + def test_swift_upload_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + OBJECT_DATA = 'someData' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.create_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.write(OBJECT_DATA).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_upload_object(CONTAINER_NAME, OBJECT_NAME, + OBJECT_DATA) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_delete_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.delete_object(OBJECT_NAME).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + ret_val = api.swift_delete_object(CONTAINER_NAME, OBJECT_NAME) + + self.assertIsNone(ret_val) + + self.mox.VerifyAll() + + def test_swift_get_object_data(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + OBJECT_DATA = 'objectData' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.storage_object.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.get_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.stream().AndReturn(OBJECT_DATA) + + self.mox.ReplayAll() + + ret_val = api.swift_get_object_data(CONTAINER_NAME, OBJECT_NAME) + + self.assertEqual(ret_val, OBJECT_DATA) + + self.mox.VerifyAll() + + def test_swift_object_exists(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + swift_object = self.mox.CreateMock(cloudfiles.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + container.get_object(OBJECT_NAME).AndReturn(swift_object) + + self.mox.ReplayAll() + + ret_val = api.swift_object_exists(CONTAINER_NAME, OBJECT_NAME) + self.assertTrue(ret_val) + + self.mox.VerifyAll() + + def test_swift_copy_object(self): + CONTAINER_NAME = 'containerName' + OBJECT_NAME = 'objectName' + + swift_api = self.stub_swift_api() + container = self.mox.CreateMock(cloudfiles.container.Container) + self.mox.StubOutWithMock(api, 'swift_object_exists') + + swift_object = self.mox.CreateMock(cloudfiles.Object) + + swift_api.get_container(CONTAINER_NAME).AndReturn(container) + api.swift_object_exists(CONTAINER_NAME, OBJECT_NAME).AndReturn(False) + + container.get_object(OBJECT_NAME).AndReturn(swift_object) + swift_object.copy_to(CONTAINER_NAME, OBJECT_NAME) + + self.mox.ReplayAll() + + ret_val = api.swift_copy_object(CONTAINER_NAME, OBJECT_NAME, + CONTAINER_NAME, OBJECT_NAME) + + self.assertIsNone(ret_val) + self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/broken/README b/django-openstack/django_openstack/tests/broken/README new file mode 100644 index 000000000..a2eec6528 --- /dev/null +++ b/django-openstack/django_openstack/tests/broken/README @@ -0,0 +1,2 @@ +Intentionally not a python module so that test runner won't find +these broken tests diff --git a/django-openstack/django_openstack/tests/base.py b/django-openstack/django_openstack/tests/broken/base.py similarity index 100% rename from django-openstack/django_openstack/tests/base.py rename to django-openstack/django_openstack/tests/broken/base.py diff --git a/django-openstack/django_openstack/tests/credential_tests.py b/django-openstack/django_openstack/tests/broken/credential_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/credential_tests.py rename to django-openstack/django_openstack/tests/broken/credential_tests.py diff --git a/django-openstack/django_openstack/tests/image_tests.py b/django-openstack/django_openstack/tests/broken/image_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/image_tests.py rename to django-openstack/django_openstack/tests/broken/image_tests.py diff --git a/django-openstack/django_openstack/tests/instance_tests.py b/django-openstack/django_openstack/tests/broken/instance_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/instance_tests.py rename to django-openstack/django_openstack/tests/broken/instance_tests.py diff --git a/django-openstack/django_openstack/tests/keypair_tests.py b/django-openstack/django_openstack/tests/broken/keypair_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/keypair_tests.py rename to django-openstack/django_openstack/tests/broken/keypair_tests.py diff --git a/django-openstack/django_openstack/tests/region_tests.py b/django-openstack/django_openstack/tests/broken/region_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/region_tests.py rename to django-openstack/django_openstack/tests/broken/region_tests.py diff --git a/django-openstack/django_openstack/tests/test_models.py b/django-openstack/django_openstack/tests/broken/test_models.py similarity index 100% rename from django-openstack/django_openstack/tests/test_models.py rename to django-openstack/django_openstack/tests/broken/test_models.py diff --git a/django-openstack/django_openstack/tests/volume_tests.py b/django-openstack/django_openstack/tests/broken/volume_tests.py similarity index 100% rename from django-openstack/django_openstack/tests/volume_tests.py rename to django-openstack/django_openstack/tests/broken/volume_tests.py diff --git a/django-openstack/django_openstack/tests/dependency_tests.py b/django-openstack/django_openstack/tests/dependency_tests.py new file mode 100644 index 000000000..af3c970dd --- /dev/null +++ b/django-openstack/django_openstack/tests/dependency_tests.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + +""" +Tests for dependency packages +Honestly, this can probably go away once tests that depend on these +packages become more ingrained in the code. +""" + +from django import test +from django.core import mail +from mailer import engine +from mailer import send_mail + + +class DjangoMailerPresenceTest(test.TestCase): + def test_mailsent(self): + send_mail('subject', 'message_body', 'from@test.com', ['to@test.com']) + engine.send_all() + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'subject') diff --git a/django-openstack/django_openstack/tests/testsettings.py b/django-openstack/django_openstack/tests/testsettings.py index 5b5360424..da2878522 100644 --- a/django-openstack/django_openstack/tests/testsettings.py +++ b/django-openstack/django_openstack/tests/testsettings.py @@ -1,3 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import os ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) @@ -16,7 +36,17 @@ INSTALLED_APPS = ['django.contrib.auth', 'django_openstack', 'django_openstack.tests', 'django_openstack.templatetags', + 'mailer', ] + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django_openstack.middleware.keystone.AuthenticationMiddleware', + ) + ROOT_URLCONF = 'django_openstack.tests.testurls' TEMPLATE_DIRS = ( os.path.join(ROOT_PATH, 'tests', 'templates') @@ -29,6 +59,23 @@ NOVA_DEFAULT_ENDPOINT = None NOVA_DEFAULT_REGION = 'test' NOVA_ACCESS_KEY = 'test' NOVA_SECRET_KEY = 'test' +OPENSTACK_ADMIN_TOKEN = 'test' CREDENTIAL_AUTHORIZATION_DAYS = 2 CREDENTIAL_DOWNLOAD_URL = TESTSERVER + '/credentials/' + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +NOSE_ARGS = ['--nocapture', + '--cover-package=django_openstack', + '--cover-inclusive', + ] + +# django-mailer uses a different config attribute +# even though it just wraps django.core.mail +MAILER_EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +EMAIL_BACKEND = MAILER_EMAIL_BACKEND + +SWIFT_ACCOUNT = 'test' +SWIFT_USER = 'tester' +SWIFT_PASS = 'testing' +SWIFT_AUTHURL = 'http://swift/swiftapi/v1.0' diff --git a/django-openstack/django_openstack/tests/testurls.py b/django-openstack/django_openstack/tests/testurls.py index 23a718a62..870ff6b93 100644 --- a/django-openstack/django_openstack/tests/testurls.py +++ b/django-openstack/django_openstack/tests/testurls.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -22,13 +24,14 @@ URL patterns for testing django-openstack views. from django.conf.urls.defaults import * +from django_openstack import urls as django_openstack_urls + urlpatterns = patterns('', - url(r'^projects/', include('django_openstack.nova.urls.project')), - url(r'^region/', include('django_openstack.nova.urls.region')), - #url(r'^admin/projects/', include('django_openstack.nova.urls.admin_project')), - #url(r'^admin/roles/', include('django_openstack.nova.urls.admin_roles')), - url(r'^credentials/download/(?P\w+)/$', - 'django_openstack.nova.views.credentials.authorize_credentials', - name='nova_credentials_authorize'), + url(r'^dash/$', 'django_openstack.dash.views.instances.usage', name='dash_overview'), + url(r'^syspanel/$', 'django_openstack.syspanel.views.instances.usage', name='syspanel_overview') ) + + +# NOTE(termie): just append them since we want the routes at the root +urlpatterns += django_openstack_urls.urlpatterns diff --git a/django-openstack/django_openstack/tests/view_tests/__init__.py b/django-openstack/django_openstack/tests/view_tests/__init__.py index 51d01c59e..e69de29bb 100644 --- a/django-openstack/django_openstack/tests/view_tests/__init__.py +++ b/django-openstack/django_openstack/tests/view_tests/__init__.py @@ -1,7 +0,0 @@ -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-openstack/django_openstack/tests/view_tests/base.py b/django-openstack/django_openstack/tests/view_tests/base.py new file mode 100644 index 000000000..4eb593f1d --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/base.py @@ -0,0 +1,106 @@ +# 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. +# +# Copyright 2011 Fourth Paradigm Development Inc. +# +# 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 http +from django import shortcuts +from django import template as django_template +from django import test +from django.conf import settings +from django_openstack.middleware import keystone + + +class Object(object): + """Inner Object for api resource wrappers""" + pass + + +def fake_render_to_response(template_name, context, context_instance=None, + mimetype='text/html'): + """Replacement for render_to_response so that views can be tested + without having to stub out templates that belong in the frontend + implementation. + + Should be able to be tested using the django unit test assertions like a + normal render_to_response return value can be. + """ + class Template(object): + def __init__(self, name): + self.name = name + + if context_instance is None: + context_instance = django_template.Context(context) + else: + context_instance.update(context) + + resp = http.HttpResponse() + template = Template(template_name) + + resp.write('

' + 'This is a fake httpresponse for testing purposes only' + '

') + + # Allows django.test.client to populate fields on the response object + test.signals.template_rendered.send(template, template=template, + context=context_instance) + + return resp + + +class BaseViewTests(test.TestCase): + TEST_PROJECT = 'test' + TEST_REGION = 'test' + TEST_STAFF_USER = 'staffUser' + TEST_TENANT = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_USER = 'test' + + @classmethod + def setUpClass(cls): + cls._real_render_to_response = shortcuts.render_to_response + shortcuts.render_to_response = fake_render_to_response + cls._real_get_user_from_request = keystone.get_user_from_request + + @classmethod + def tearDownClass(cls): + shortcuts.render_to_response = cls._real_render_to_response + keystone.get_user_from_request = cls._real_get_user_from_request + + def setUp(self): + self.mox = mox.Mox() + self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, + True) + + 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 setActiveUser(self, token, username, tenant, is_admin): + keystone.get_user_from_request = \ + lambda x: keystone.User(token, username, tenant, is_admin) diff --git a/django-openstack/django_openstack/tests/view_tests/dash/__init__.py b/django-openstack/django_openstack/tests/view_tests/dash/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py new file mode 100644 index 000000000..9b6099542 --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/dash/container_tests.py @@ -0,0 +1,94 @@ +from cloudfiles.errors import ContainerNotEmpty +from django.contrib import messages +from django.core.urlresolvers import reverse +from django_openstack import api +from django_openstack.tests.view_tests import base +from mox import IgnoreArg, IsA + + +class ContainerViewTests(base.BaseViewTests): + def setUp(self): + super(ContainerViewTests, self).setUp() + container_inner = base.Object() + container_inner.name = 'containerName' + self.container = api.Container(container_inner) + + def test_index(self): + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers().AndReturn([self.container]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_containers', args=['tenant'])) + + self.assertTemplateUsed(res, 'dash_containers.html') + self.assertIn('containers', res.context) + containers = res.context['containers'] + + self.assertEqual(len(containers), 1) + self.assertEqual(containers[0].name, 'containerName') + + self.mox.VerifyAll() + + def test_delete_container(self): + formData = {'container_name': 'containerName', + 'method': 'DeleteContainer'} + + self.mox.StubOutWithMock(api, 'swift_delete_container') + api.swift_delete_container('containerName') + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_containers', args=['tenant']), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_containers', + args=['tenant'])) + + self.mox.VerifyAll() + + def test_delete_container_nonempty(self): + formData = {'container_name': 'containerName', + 'method': 'DeleteContainer'} + + exception = ContainerNotEmpty('containerNotEmpty') + + self.mox.StubOutWithMock(api, 'swift_delete_container') + api.swift_delete_container('containerName').AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + + messages.error(IgnoreArg(), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_containers', args=['tenant']), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_containers', + args=['tenant'])) + + self.mox.VerifyAll() + + def test_create_container_get(self): + res = self.client.get(reverse('dash_containers_create', + args=['tenant'])) + + self.assertTemplateUsed(res, 'dash_containers_create.html') + + def test_create_container_post(self): + formData = {'name': 'containerName', + 'method': 'CreateContainer'} + + self.mox.StubOutWithMock(api, 'swift_create_container') + api.swift_create_container('CreateContainer') + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IgnoreArg(), IsA(str)) + + res = self.client.post(reverse('dash_containers_create', + args=['tenant']), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_containers_create', + args=['tenant'])) diff --git a/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py new file mode 100644 index 000000000..da0207edd --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/dash/images_tests.py @@ -0,0 +1,330 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from django_openstack import api +from django_openstack.tests.view_tests import base +from glance.common import exception as glance_exception +from openstackx.api import exceptions as api_exceptions +from mox import IgnoreArg, IsA + + +class ImageViewTests(base.BaseViewTests): + def setUp(self): + super(ImageViewTests, self).setUp() + image_dict = {'name': 'visibleImage', + 'container_format': 'novaImage'} + self.visibleImage = api.Image(image_dict) + + image_dict = {'name': 'invisibleImage', + 'container_format': 'aki'} + self.invisibleImage = api.Image(image_dict) + + self.images = (self.visibleImage, self.invisibleImage) + + flavor_inner = base.Object() + flavor_inner.id = 1 + flavor_inner.name = 'm1.massive' + flavor_inner.vcpus = 1000 + flavor_inner.disk = 1024 + flavor_inner.ram = 10000 + self.flavors = (api.Flavor(flavor_inner),) + + keypair_inner = base.Object() + keypair_inner.key_name = 'keyName' + self.keypairs = (api.KeyPair(keypair_inner),) + + def test_index(self): + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'image_list_detailed') + api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(self.images) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_images.html') + + self.assertIn('images', res.context) + images = res.context['images'] + self.assertEqual(len(images), 1) + self.assertEqual(images[0].name, 'visibleImage') + + self.mox.VerifyAll() + + def test_index_no_images(self): + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'image_list_detailed') + api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) + + self.mox.StubOutWithMock(messages, 'info') + messages.info(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_images.html') + + self.mox.VerifyAll() + + def test_index_client_conn_error(self): + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'image_list_detailed') + exception = glance_exception.ClientConnectionError('clientConnError') + api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_images.html') + + self.mox.VerifyAll() + + def test_index_glance_error(self): + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'image_list_detailed') + exception = glance_exception.Error('glanceError') + api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_images.html') + + self.mox.VerifyAll() + + def test_launch_get(self): + IMAGE_ID = '1' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID])) + + self.assertTemplateUsed(res, 'dash_launch.html') + + image = res.context['image'] + self.assertEqual(image.name, self.visibleImage.name) + + self.assertEqual(res.context['tenant'], self.TEST_TENANT) + + form = res.context['form'] + + form_flavorfield = form.fields['flavor'] + self.assertIn('m1.massive', form_flavorfield.choices[0][1]) + + form_keyfield = form.fields['key_name'] + self.assertEqual(form_keyfield.choices[0][0], + self.keypairs[0].key_name) + + self.mox.VerifyAll() + + def test_launch_post(self): + FLAVOR_ID = self.flavors[0].id + IMAGE_ID = '1' + KEY_NAME = self.keypairs[0].key_name + SERVER_NAME = 'serverName' + USER_DATA = 'userData' + + form_data = {'method': 'LaunchForm', + 'flavor': FLAVOR_ID, + 'image_id': IMAGE_ID, + 'key_name': KEY_NAME, + 'name': SERVER_NAME, + 'user_data': USER_DATA, + } + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + # called again by the form + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'flavor_get') + api.flavor_get(IsA(http.HttpRequest), + IsA(unicode)).AndReturn(self.flavors[0]) + + self.mox.StubOutWithMock(api, 'server_create') + + api.server_create(IsA(http.HttpRequest), SERVER_NAME, + self.visibleImage, self.flavors[0], + user_data=USER_DATA, key_name=KEY_NAME) + + self.mox.StubOutWithMock(messages, 'success') + messages.success(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID]), + form_data) + + self.assertRedirectsNoFollow(res, reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID])) + + self.mox.VerifyAll() + + def test_launch_flavorlist_error(self): + IMAGE_ID = '1' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(self.TEST_TENANT) + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID])) + + self.assertTemplateUsed(res, 'dash_launch.html') + + form = res.context['form'] + + form_flavorfield = form.fields['flavor'] + self.assertIn('m1.tiny', form_flavorfield.choices[0][1]) + + self.mox.VerifyAll() + + def test_launch_keypairlist_error(self): + IMAGE_ID = '1' + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + exception = api_exceptions.ApiException('apiException') + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID])) + + self.assertTemplateUsed(res, 'dash_launch.html') + + form = res.context['form'] + + form_keyfield = form.fields['key_name'] + self.assertEqual(len(form_keyfield.choices), 0) + + self.mox.VerifyAll() + + def test_launch_form_apiexception(self): + FLAVOR_ID = self.flavors[0].id + IMAGE_ID = '1' + KEY_NAME = self.keypairs[0].key_name + SERVER_NAME = 'serverName' + USER_DATA = 'userData' + + form_data = {'method': 'LaunchForm', + 'flavor': FLAVOR_ID, + 'image_id': IMAGE_ID, + 'key_name': KEY_NAME, + 'name': SERVER_NAME, + 'user_data': USER_DATA, + } + + self.mox.StubOutWithMock(api, 'image_get') + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'token_get_tenant') + api.token_get_tenant(IsA(http.HttpRequest), + self.TEST_TENANT).AndReturn(self.TEST_TENANT) + + self.mox.StubOutWithMock(api, 'flavor_list') + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + # called again by the form + api.image_get(IsA(http.HttpRequest), + IMAGE_ID).AndReturn(self.visibleImage) + + self.mox.StubOutWithMock(api, 'flavor_get') + api.flavor_get(IsA(http.HttpRequest), + IsA(unicode)).AndReturn(self.flavors[0]) + + self.mox.StubOutWithMock(api, 'server_create') + + exception = api_exceptions.ApiException('apiException') + api.server_create(IsA(http.HttpRequest), SERVER_NAME, + self.visibleImage, self.flavors[0], + user_data=USER_DATA, + key_name=KEY_NAME).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_images_launch', + args=[self.TEST_TENANT, IMAGE_ID]), + form_data) + + self.assertTemplateUsed(res, 'dash_launch.html') + + self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py new file mode 100644 index 000000000..ba5025d85 --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/dash/instance_tests.py @@ -0,0 +1,317 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +import datetime + +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from django_openstack import api +from django_openstack import utils +from django_openstack.tests.view_tests import base +from openstackx.api import exceptions as api_exceptions +from mox import IsA + + +class InstanceViewTests(base.BaseViewTests): + def setUp(self): + super(InstanceViewTests, self).setUp() + server_inner = base.Object() + server_inner.id = 1 + server_inner.name = 'serverName' + self.servers = (api.Server(server_inner, None),) + + def test_index(self): + self.mox.StubOutWithMock(api, 'server_list') + api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_instances.html') + self.assertItemsEqual(res.context['instances'], self.servers) + + self.mox.VerifyAll() + + def test_index_server_list_exception(self): + self.mox.StubOutWithMock(api, 'server_list') + exception = api_exceptions.ApiException('apiException') + api.server_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_instances.html') + self.assertEqual(len(res.context['instances']), 0) + + self.mox.VerifyAll() + + def test_terminate_instance(self): + formData = {'method': 'TerminateInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.servers[0].id)).AndReturn(self.servers[0]) + self.mox.StubOutWithMock(api, 'server_delete') + api.server_delete(IsA(http.HttpRequest), + self.servers[0]) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_instances', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_terminate_instance_exception(self): + formData = {'method': 'TerminateInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_get') + api.server_get(IsA(http.HttpRequest), + str(self.servers[0].id)).AndReturn(self.servers[0]) + + exception = api_exceptions.ApiException('ApiException', + message='apiException') + self.mox.StubOutWithMock(api, 'server_delete') + api.server_delete(IsA(http.HttpRequest), + self.servers[0]).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_instances', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_reboot_instance(self): + formData = {'method': 'RebootInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_reboot') + api.server_reboot(IsA(http.HttpRequest), unicode(self.servers[0].id)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_instances', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_reboot_instance_exception(self): + formData = {'method': 'RebootInstance', + 'instance': self.servers[0].id, + } + + self.mox.StubOutWithMock(api, 'server_reboot') + exception = api_exceptions.ApiException('ApiException', + message='apiException') + api.server_reboot(IsA(http.HttpRequest), + unicode(self.servers[0].id)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_instances', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def override_times(self, time=datetime.datetime.now): + now = datetime.datetime.utcnow() + utils.time.override_time = \ + datetime.time(now.hour, now.minute, now.second) + utils.today.override_time = datetime.date(now.year, now.month, now.day) + utils.utcnow.override_time = now + + return now + + def reset_times(self): + utils.time.override_time = None + utils.today.override_time = None + utils.utcnow.override_time = None + + def test_instance_usage(self): + TEST_RETURN = 'testReturn' + + now = self.override_times() + + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_usage', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_usage.html') + + self.assertEqual(res.context['usage'], TEST_RETURN) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_usage_exception(self): + now = self.override_times() + + exception = api_exceptions.ApiException('apiException', + message='apiException') + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_usage', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_usage.html') + + self.assertEqual(res.context['usage'], {}) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_usage_default_tenant(self): + TEST_RETURN = 'testReturn' + + now = self.override_times() + + self.mox.StubOutWithMock(api, 'usage_get') + api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT, + datetime.datetime(now.year, now.month, 1, + now.hour, now.minute, now.second), + now).AndReturn(TEST_RETURN) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_overview')) + + self.assertTemplateUsed(res, 'dash_usage.html') + + self.assertEqual(res.context['usage'], TEST_RETURN) + + self.mox.VerifyAll() + + self.reset_times() + + def test_instance_console(self): + CONSOLE_OUTPUT = 'output' + INSTANCE_ID = self.servers[0].id + + console_mock = self.mox.CreateMock(api.Console) + console_mock.output = CONSOLE_OUTPUT + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndReturn(console_mock) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances_console', + args=[self.TEST_TENANT, INSTANCE_ID])) + + self.assertIsInstance(res, http.HttpResponse) + self.assertContains(res, CONSOLE_OUTPUT) + + self.mox.VerifyAll() + + def test_instance_console_exception(self): + INSTANCE_ID = self.servers[0].id + + exception = api_exceptions.ApiException('apiException', + message='apiException') + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IsA(http.HttpRequest), + unicode(INSTANCE_ID)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(unicode)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances_console', + args=[self.TEST_TENANT, INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_instance_vnc(self): + INSTANCE_ID = self.servers[0].id + CONSOLE_OUTPUT = '/vncserver' + + console_mock = self.mox.CreateMock(api.Console) + console_mock.output = CONSOLE_OUTPUT + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IsA(http.HttpRequest), + unicode(INSTANCE_ID), + 'vnc').AndReturn(console_mock) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances_vnc', + args=[self.TEST_TENANT, INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, CONSOLE_OUTPUT) + + self.mox.VerifyAll() + + def test_instance_vnc_exception(self): + INSTANCE_ID = self.servers[0].id + + exception = api_exceptions.ApiException('apiException', + message='apiException') + + self.mox.StubOutWithMock(api, 'console_create') + api.console_create(IsA(http.HttpRequest), + unicode(INSTANCE_ID), + 'vnc').AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_instances_vnc', + args=[self.TEST_TENANT, INSTANCE_ID])) + + self.assertRedirectsNoFollow(res, reverse('dash_instances', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py new file mode 100644 index 000000000..dec378527 --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/dash/keypair_tests.py @@ -0,0 +1,145 @@ +from django import http +from django.contrib import messages +from django.core.urlresolvers import reverse +from django_openstack import api +from django_openstack.tests.view_tests import base +from mox import IsA + +import openstackx.api.exceptions as api_exceptions + +class KeyPairViewTests(base.BaseViewTests): + def setUp(self): + super(KeyPairViewTests, self).setUp() + keypair_inner = base.Object() + keypair_inner.key_name = 'keyName' + self.keypairs = (api.KeyPair(keypair_inner),) + + def test_index(self): + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_keypairs', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_keypairs.html') + self.assertItemsEqual(res.context['keypairs'], self.keypairs) + + self.mox.VerifyAll() + + def test_index_exception(self): + exception = api_exceptions.ApiException('apiException', + message='apiException') + self.mox.StubOutWithMock(api, 'keypair_list') + api.keypair_list(IsA(http.HttpRequest)).AndRaise(exception) + + self.mox.StubOutWithMock(messages, 'error') + messages.error(IsA(http.HttpRequest), IsA(str)) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_keypairs', args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_keypairs.html') + self.assertEqual(len(res.context['keypairs']), 0) + + self.mox.VerifyAll() + + def test_delete_keypair(self): + KEYPAIR_ID = self.keypairs[0].key_name + formData = {'method': 'DeleteKeypair', + 'keypair_id': KEYPAIR_ID, + } + + self.mox.StubOutWithMock(api, 'keypair_delete') + api.keypair_delete(IsA(http.HttpRequest), unicode(KEYPAIR_ID)) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_keypairs', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_keypairs', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_delete_keypair_exception(self): + KEYPAIR_ID = self.keypairs[0].key_name + formData = {'method': 'DeleteKeypair', + 'keypair_id': KEYPAIR_ID, + } + + exception = api_exceptions.ApiException('apiException', + message='apiException') + self.mox.StubOutWithMock(api, 'keypair_delete') + api.keypair_delete(IsA(http.HttpRequest), + unicode(KEYPAIR_ID)).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_keypairs', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_keypairs', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() + + def test_create_keypair_get(self): + res = self.client.get(reverse('dash_keypairs_create', + args=[self.TEST_TENANT])) + + self.assertTemplateUsed(res, 'dash_keypairs_create.html') + + def test_create_keypair_post(self): + KEYPAIR_NAME = 'newKeypair' + PRIVATE_KEY = 'privateKey' + + newKeyPair = self.mox.CreateMock(api.KeyPair) + newKeyPair.key_name = KEYPAIR_NAME + newKeyPair.private_key = PRIVATE_KEY + + formData = {'method': 'CreateKeypair', + 'name': KEYPAIR_NAME, + } + + self.mox.StubOutWithMock(api, 'keypair_create') + api.keypair_create(IsA(http.HttpRequest), + KEYPAIR_NAME).AndReturn(newKeyPair) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_keypairs_create', + args=[self.TEST_TENANT]), + formData) + + self.assertTrue(res.has_header('Content-Disposition')) + + self.mox.VerifyAll() + + def test_create_keypair_exception(self): + KEYPAIR_NAME = 'newKeypair' + + formData = {'method': 'CreateKeypair', + 'name': KEYPAIR_NAME, + } + + exception = api_exceptions.ApiException('apiException', + message='apiException') + self.mox.StubOutWithMock(api, 'keypair_create') + api.keypair_create(IsA(http.HttpRequest), + KEYPAIR_NAME).AndRaise(exception) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_keypairs_create', + args=[self.TEST_TENANT]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_keypairs_create', + args=[self.TEST_TENANT])) + + self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py b/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py new file mode 100644 index 000000000..8a37b894e --- /dev/null +++ b/django-openstack/django_openstack/tests/view_tests/dash/object_tests.py @@ -0,0 +1,190 @@ +import tempfile + +from django.core.urlresolvers import reverse +from django_openstack import api +from django_openstack.tests.view_tests import base + + +class ObjectViewTests(base.BaseViewTests): + CONTAINER_NAME = 'containerName' + + def setUp(self): + super(ObjectViewTests, self).setUp() + swift_object = self.mox.CreateMock(api.SwiftObject) + self.swift_objects = [swift_object] + + def test_index(self): + self.mox.StubOutWithMock(api, 'swift_get_objects') + api.swift_get_objects( + self.CONTAINER_NAME).AndReturn(self.swift_objects) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_objects', + args=[self.TEST_TENANT, + self.CONTAINER_NAME])) + self.assertTemplateUsed(res, 'dash_objects.html') + self.assertItemsEqual(res.context['objects'], self.swift_objects) + + self.mox.VerifyAll() + + def test_upload_index(self): + res = self.client.get(reverse('dash_objects_upload', + args=[self.TEST_TENANT, + self.CONTAINER_NAME])) + + self.assertTemplateUsed(res, 'dash_objects_upload.html') + + def test_upload(self): + OBJECT_DATA = 'objectData' + OBJECT_FILE = tempfile.TemporaryFile() + OBJECT_FILE.write(OBJECT_DATA) + OBJECT_FILE.flush() + OBJECT_FILE.seek(0) + OBJECT_NAME = 'objectName' + + formData = {'method': 'UploadObject', + 'container_name': self.CONTAINER_NAME, + 'name': OBJECT_NAME, + 'object_file': OBJECT_FILE} + + self.mox.StubOutWithMock(api, 'swift_upload_object') + api.swift_upload_object(unicode(self.CONTAINER_NAME), + unicode(OBJECT_NAME), + OBJECT_DATA) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_objects_upload', + args=[self.TEST_TENANT, + self.CONTAINER_NAME]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_objects_upload', + args=[self.TEST_TENANT, + self.CONTAINER_NAME])) + + self.mox.VerifyAll() + + def test_delete(self): + OBJECT_NAME = 'objectName' + formData = {'method': 'DeleteObject', + 'container_name': self.CONTAINER_NAME, + 'object_name': OBJECT_NAME} + + self.mox.StubOutWithMock(api, 'swift_delete_object') + api.swift_delete_object(self.CONTAINER_NAME, OBJECT_NAME) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_objects', + args=[self.TEST_TENANT, + self.CONTAINER_NAME]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_objects', + args=[self.TEST_TENANT, + self.CONTAINER_NAME])) + + self.mox.VerifyAll() + + def test_download(self): + OBJECT_DATA = 'objectData' + OBJECT_NAME = 'objectName' + + self.mox.StubOutWithMock(api, 'swift_get_object_data') + api.swift_get_object_data(unicode(self.CONTAINER_NAME), + unicode(OBJECT_NAME)).AndReturn(OBJECT_DATA) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_objects_download', + args=[self.TEST_TENANT, + self.CONTAINER_NAME, + OBJECT_NAME])) + + self.assertEqual(res.content, OBJECT_DATA) + self.assertTrue(res.has_header('Content-Disposition')) + + self.mox.VerifyAll() + + def test_copy_index(self): + OBJECT_NAME = 'objectName' + + container = self.mox.CreateMock(api.Container) + container.name = self.CONTAINER_NAME + + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers().AndReturn([container]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('dash_object_copy', + args=[self.TEST_TENANT, + self.CONTAINER_NAME, + OBJECT_NAME])) + + self.assertTemplateUsed(res, 'dash_object_copy.html') + + self.mox.VerifyAll() + + def test_copy(self): + NEW_CONTAINER_NAME = self.CONTAINER_NAME + NEW_OBJECT_NAME = 'newObjectName' + ORIG_CONTAINER_NAME = 'origContainerName' + ORIG_OBJECT_NAME = 'origObjectName' + + formData = {'method': 'CopyObject', + 'new_container_name': NEW_CONTAINER_NAME, + 'new_object_name': NEW_OBJECT_NAME, + 'orig_container_name': ORIG_CONTAINER_NAME, + 'orig_object_name': ORIG_OBJECT_NAME} + + container = self.mox.CreateMock(api.Container) + container.name = self.CONTAINER_NAME + + self.mox.StubOutWithMock(api, 'swift_get_containers') + api.swift_get_containers().AndReturn([container]) + + self.mox.StubOutWithMock(api, 'swift_copy_object') + api.swift_copy_object(ORIG_CONTAINER_NAME, ORIG_OBJECT_NAME, + NEW_CONTAINER_NAME, NEW_OBJECT_NAME) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_object_copy', + args=[self.TEST_TENANT, + ORIG_CONTAINER_NAME, + ORIG_OBJECT_NAME]), + formData) + + self.assertRedirectsNoFollow(res, reverse('dash_object_copy', + args=[self.TEST_TENANT, + ORIG_CONTAINER_NAME, + ORIG_OBJECT_NAME])) + + self.mox.VerifyAll() + + def test_filter(self): + PREFIX = 'prefix' + + formData = {'method': 'FilterObjects', + 'container_name': self.CONTAINER_NAME, + 'object_prefix': PREFIX, + } + + self.mox.StubOutWithMock(api, 'swift_get_objects') + api.swift_get_objects(unicode(self.CONTAINER_NAME), + prefix=unicode(PREFIX) + ).AndReturn(self.swift_objects) + + self.mox.ReplayAll() + + res = self.client.post(reverse('dash_objects', + args=[self.TEST_TENANT, + self.CONTAINER_NAME]), + formData) + + self.assertTemplateUsed(res, 'dash_objects.html') + + self.mox.VerifyAll() diff --git a/django-openstack/django_openstack/urls.py b/django-openstack/django_openstack/urls.py index b7525f810..bd4491cff 100644 --- a/django-openstack/django_openstack/urls.py +++ b/django-openstack/django_openstack/urls.py @@ -1,5 +1,23 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.conf.urls.defaults import * from django.conf import settings diff --git a/django-openstack/django_openstack/utils.py b/django-openstack/django_openstack/utils.py index d1537c85f..65fa39d0c 100644 --- a/django-openstack/django_openstack/utils.py +++ b/django-openstack/django_openstack/utils.py @@ -1,5 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import datetime + +def time(): + '''Overrideable version of datetime.datetime.today''' + if time.override_time: + return time.override_time + return datetime.time() + +time.override_time = None + + +def today(): + '''Overridable version of datetime.datetime.today''' + if today.override_time: + return today.override_time + return datetime.datetime.today() + +today.override_time = None + + def utcnow(): '''Overridable version of datetime.datetime.utcnow''' if utcnow.override_time: diff --git a/django-openstack/setup.py b/django-openstack/setup.py index 909bf4b2d..078a82f7c 100755 --- a/django-openstack/setup.py +++ b/django-openstack/setup.py @@ -1,3 +1,23 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import os from setuptools import setup, find_packages, findall @@ -17,7 +37,7 @@ setup( package_data = {'django_openstack': [s[len('django_openstack/'):] for s in findall('django_openstack/templates')]}, - install_requires = ['setuptools', 'mox>=0.5.0'], + install_requires = ['setuptools', 'mox>=0.5.3', 'django_nose'], classifiers = [ 'Development Status :: 4 - Beta', 'Framework :: Django', diff --git a/openstack-dashboard/README b/openstack-dashboard/README index 302277bef..3c9053a21 100644 --- a/openstack-dashboard/README +++ b/openstack-dashboard/README @@ -33,5 +33,21 @@ If all is well you should now able to run the server locally: $ tools/with_venv.sh dashboard/manage.py runserver +Adding openstackx Extensions to Nova +------------------------------------ +If you are seeing large numbers of 404 exceptions on operations such as listing +servers, you are probably not running the openstackx extensions that the +dashboard depends on. You will need to download the openstackx code from +> https://github.com/cloudbuilders/openstackx + +and add the following option to your nova instantiation: + +> --osapi_extensions_path=/path/to/openstackx/extensions + +The rackspace cloudbuilders nova.sh script automates this process and creates a +full nova installation compatible with the dashboard. You can acquire this +script from the repository at + +https://github.com/cloudbuilders/deploy.sh diff --git a/openstack-dashboard/dashboard/manage.py b/openstack-dashboard/dashboard/manage.py index 5e78ea979..44b514cae 100755 --- a/openstack-dashboard/dashboard/manage.py +++ b/openstack-dashboard/dashboard/manage.py @@ -1,4 +1,24 @@ #!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + from django.core.management import execute_manager try: import settings # Assumed to be in the same directory. diff --git a/openstack-dashboard/dashboard/middleware.py b/openstack-dashboard/dashboard/middleware.py index 71add0d2f..0d5988141 100644 --- a/openstack-dashboard/dashboard/middleware.py +++ b/openstack-dashboard/dashboard/middleware.py @@ -1,11 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import logging -import traceback LOG = logging.getLogger('openstack_dashboard') class DashboardLogUnhandledExceptionsMiddleware(object): def process_exception(self, request, exception): - tb_text = traceback.format_exc() - LOG.critical('Unhandled Exception in dashboard. Exception "%s"' - '\n%s' % (str(exception), tb_text)) + LOG.critical('Unhandled Exception in of type "%s" in dashboard.' + % type(exception), exc_info=True) diff --git a/openstack-dashboard/dashboard/settings.py b/openstack-dashboard/dashboard/settings.py index c111474f9..435a7ef58 100644 --- a/openstack-dashboard/dashboard/settings.py +++ b/openstack-dashboard/dashboard/settings.py @@ -1,4 +1,23 @@ -import boto +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2011 Fourth Paradigm Development, Inc. +# +# 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. + import logging import os import sys @@ -45,6 +64,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.media', 'django.contrib.messages.context_processors.messages', 'django_openstack.context_processors.tenants', + 'django_openstack.context_processors.swift', ) TEMPLATE_LOADERS = ( @@ -67,6 +87,7 @@ INSTALLED_APPS = ( 'django.contrib.syndication', 'django_openstack', 'django_openstack.templatetags', + 'mailer', ) AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) @@ -90,10 +111,7 @@ USE_I18N = True ACCOUNT_ACTIVATION_DAYS = 7 -# NOTE(devcamcar): Prevent boto from retrying and stalling the connection. -if not boto.config.has_section('Boto'): - boto.config.add_section('Boto') -boto.config.set('Boto', 'num_retries', '0') +TOTAL_CLOUD_RAM_GB = 10 try: from local.local_settings import * diff --git a/openstack-dashboard/dashboard/templates/_container_form.html b/openstack-dashboard/dashboard/templates/_container_form.html new file mode 100644 index 000000000..c0a201b74 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_container_form.html @@ -0,0 +1,10 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/openstack-dashboard/dashboard/templates/_container_list.html b/openstack-dashboard/dashboard/templates/_container_list.html new file mode 100644 index 000000000..a3f8dfc2e --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_container_list.html @@ -0,0 +1,18 @@ + + + + + + {% for container in containers %} + + + + + {% endfor %} +
NameActions
{{ container.name }} + +
diff --git a/openstack-dashboard/dashboard/templates/_copy_object.html b/openstack-dashboard/dashboard/templates/_copy_object.html new file mode 100644 index 000000000..85c3875ac --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_copy_object.html @@ -0,0 +1,10 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/openstack-dashboard/dashboard/templates/_dash_sidebar.html b/openstack-dashboard/dashboard/templates/_dash_sidebar.html index f4b6e4681..027fcf5bb 100644 --- a/openstack-dashboard/dashboard/templates/_dash_sidebar.html +++ b/openstack-dashboard/dashboard/templates/_dash_sidebar.html @@ -6,4 +6,10 @@
  • Images
  • Keypairs
  • + {% if swift_configured %} +

    Manage Object Store

    + + {% endif %} diff --git a/openstack-dashboard/dashboard/templates/_delete_container.html b/openstack-dashboard/dashboard/templates/_delete_container.html new file mode 100644 index 000000000..dca872274 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_delete_container.html @@ -0,0 +1,8 @@ +
    + {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
    diff --git a/openstack-dashboard/dashboard/templates/_delete_object.html b/openstack-dashboard/dashboard/templates/_delete_object.html new file mode 100644 index 000000000..27d512440 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_delete_object.html @@ -0,0 +1,8 @@ +
    + {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
    diff --git a/openstack-dashboard/dashboard/templates/_keypair_list.html b/openstack-dashboard/dashboard/templates/_keypair_list.html index 5c684cac6..d2965906c 100644 --- a/openstack-dashboard/dashboard/templates/_keypair_list.html +++ b/openstack-dashboard/dashboard/templates/_keypair_list.html @@ -6,7 +6,7 @@ {% for keypair in keypairs %} - {{ keypair.name }} + {{ keypair.key_name }} {{ keypair.fingerprint }}
      diff --git a/openstack-dashboard/dashboard/templates/_object_filter.html b/openstack-dashboard/dashboard/templates/_object_filter.html new file mode 100644 index 000000000..42047f4b2 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_object_filter.html @@ -0,0 +1,10 @@ +
      + {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + {% for field in form.visible_fields %} + {{field}} + {% endfor %} + +
      diff --git a/openstack-dashboard/dashboard/templates/_object_form.html b/openstack-dashboard/dashboard/templates/_object_form.html new file mode 100644 index 000000000..062487852 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_object_form.html @@ -0,0 +1,10 @@ +
      + {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
      diff --git a/openstack-dashboard/dashboard/templates/_object_list.html b/openstack-dashboard/dashboard/templates/_object_list.html new file mode 100644 index 000000000..4da0a6660 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/_object_list.html @@ -0,0 +1,18 @@ + + + + + + {% for object in objects %} + + + + + {% endfor %} +
      NameActions
      {{ object.name }} +
        +
      • Copy
      • +
      • {% include "_delete_object.html" with form=delete_form %}
      • +
      • Download +
      +
      diff --git a/openstack-dashboard/dashboard/templates/_syspanel_instance_list.html b/openstack-dashboard/dashboard/templates/_syspanel_instance_list.html index faf7a24d4..d85f8df41 100644 --- a/openstack-dashboard/dashboard/templates/_syspanel_instance_list.html +++ b/openstack-dashboard/dashboard/templates/_syspanel_instance_list.html @@ -17,7 +17,7 @@ {{instance.attrs.user_id}} {{instance.attrs.host}} {{instance.attrs.launched_at|parse_date}} - {{instance.attrs.image_name}} + {{instance.image_name}} {{instance.addresses.public.0.addr|default:'N/A'}} {{instance.addresses.private.0.addr|default:'-'}} diff --git a/openstack-dashboard/dashboard/templates/dash_containers.html b/openstack-dashboard/dashboard/templates/dash_containers.html new file mode 100644 index 000000000..1a07ce5b4 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/dash_containers.html @@ -0,0 +1,37 @@ +{% extends 'dash_base.html' %} +{# list of user's containers #} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block main %} + + {% include "_messages.html" %} + +
      + {% if containers %} +
      +

      Containers

      + +
      + + {% include '_container_list.html' %} + + {% endif %} + Create New Container >> +
      +{% endblock %} + diff --git a/openstack-dashboard/dashboard/templates/dash_containers_create.html b/openstack-dashboard/dashboard/templates/dash_containers_create.html new file mode 100644 index 000000000..56ba9c500 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/dash_containers_create.html @@ -0,0 +1,33 @@ +{% extends 'dash_base.html' %} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block main %} + + + {% include "_messages.html" %} + +
      +
      +
      +

      Create Container

      +
      +
      + {% include '_container_form.html' with form=create_form %} +

      << Return to containers list

      +
      + +
      +

      Description:

      +

      A container is a storage compartment for your data and provides a way for you to organize your data. You can think of a container as a folder in Windows® or a directory in UNIX®. The primary difference between a container and these other file system concepts is that containers cannot be nested. You can, however, create an unlimited number of containers within your account. Data must be stored in a container so you must have at least one container defined in your account prior to uploading data.

      +
      +
      +
      +{% endblock %} diff --git a/openstack-dashboard/dashboard/templates/dash_object_copy.html b/openstack-dashboard/dashboard/templates/dash_object_copy.html new file mode 100644 index 000000000..70cc85dcf --- /dev/null +++ b/openstack-dashboard/dashboard/templates/dash_object_copy.html @@ -0,0 +1,36 @@ +{% extends 'dash_base.html' %} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block main %} + + + {% include "_messages.html" %} + +
      Container:
      +
      {{ container_name }}
      +
      +
      +
      +
      +

      Copy {{ object_name }}

      +
      +
      + {% include '_copy_object.html' with form=copy_form greeting="HI" %} +

      << Return to objects list

      +
      + +
      +

      Description:

      +

      You may make a new copy of an existing object to store in this or another container.

      +
      +
      +
      +{% endblock %} diff --git a/openstack-dashboard/dashboard/templates/dash_objects.html b/openstack-dashboard/dashboard/templates/dash_objects.html new file mode 100644 index 000000000..ba790797b --- /dev/null +++ b/openstack-dashboard/dashboard/templates/dash_objects.html @@ -0,0 +1,33 @@ +{% extends 'dash_base.html' %} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block main %} + + {% include "_messages.html" %} +
      Container:
      +
      {{ container_name }}
      +
      +
      + {% if objects %} +
      +

      Objects

      +
      + {% include '_object_filter.html' with form=filter_form %} +
      +
      + + {% include '_object_list.html' %} + + {% endif %} + Upload New Object >> +
      +{% endblock %} + diff --git a/openstack-dashboard/dashboard/templates/dash_objects_upload.html b/openstack-dashboard/dashboard/templates/dash_objects_upload.html new file mode 100644 index 000000000..0f8023e15 --- /dev/null +++ b/openstack-dashboard/dashboard/templates/dash_objects_upload.html @@ -0,0 +1,36 @@ +{% extends 'dash_base.html' %} + +{% block sidebar %} + {% with current_sidebar="containers" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block main %} + + + {% include "_messages.html" %} + +
      Container:
      +
      {{ container_name }}
      +
      +
      +
      +
      +

      Upload Object

      +
      +
      + {% include '_object_form.html' with form=upload_form %} +

      << Return to objects list

      +
      + +
      +

      Description:

      +

      An object is the basic storage entity and any optional metadata that represents the files you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs.

      +
      +
      +
      +{% endblock %} diff --git a/openstack-dashboard/dashboard/templates/login_required.html b/openstack-dashboard/dashboard/templates/login_required.html index c5c714a3e..27e956648 100644 --- a/openstack-dashboard/dashboard/templates/login_required.html +++ b/openstack-dashboard/dashboard/templates/login_required.html @@ -5,9 +5,7 @@ {% block sidebar %} - + {% endblock %} {% block main %} diff --git a/openstack-dashboard/dashboard/templates/switch_tenants.html b/openstack-dashboard/dashboard/templates/switch_tenants.html index 10379ede3..64ebeb767 100644 --- a/openstack-dashboard/dashboard/templates/switch_tenants.html +++ b/openstack-dashboard/dashboard/templates/switch_tenants.html @@ -3,21 +3,21 @@ Login - +
      -

      Log-in to tenant: {{to_tenant}}

      +

      Log-in to tenant: {{to_tenant}}


      {% include "_messages.html" %} {% include '_login.html' %} -
      -
      +
      diff --git a/openstack-dashboard/dashboard/tests.py b/openstack-dashboard/dashboard/tests.py new file mode 100644 index 000000000..16edcdc22 --- /dev/null +++ b/openstack-dashboard/dashboard/tests.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +''' Test for django mailer. + +This test is pretty much worthless, and should be removed once real testing of +views that send emails is implemented +''' + +from django import test +from django.core import mail +from mailer import engine +from mailer import send_mail + + +class DjangoMailerPresenceTest(test.TestCase): + def test_mailsent(self): + send_mail('subject', 'message_body', 'from@test.com', ['to@test.com']) + engine.send_all() + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'subject') diff --git a/openstack-dashboard/dashboard/urls.py b/openstack-dashboard/dashboard/urls.py index 7762b0d8b..1eee8fa53 100644 --- a/openstack-dashboard/dashboard/urls.py +++ b/openstack-dashboard/dashboard/urls.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 diff --git a/openstack-dashboard/dashboard/views.py b/openstack-dashboard/dashboard/views.py index 2711cf119..6d192e21c 100644 --- a/openstack-dashboard/dashboard/views.py +++ b/openstack-dashboard/dashboard/views.py @@ -1,9 +1,11 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # +# Copyright 2011 Fourth Paradigm Development, Inc. +# # 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 @@ -19,8 +21,6 @@ """ Views for home page. """ -import logging - from django import template from django import shortcuts from django.views.decorators import vary @@ -28,6 +28,7 @@ from django.views.decorators import vary from django_openstack import api from django_openstack.auth import views as auth_views + @vary.vary_on_cookie def splash(request): if request.user: diff --git a/openstack-dashboard/local/local_settings.py.example b/openstack-dashboard/local/local_settings.py.example index 0eb97d396..88cd33518 100644 --- a/openstack-dashboard/local/local_settings.py.example +++ b/openstack-dashboard/local/local_settings.py.example @@ -15,16 +15,33 @@ DATABASES = { CACHE_BACKEND = 'dummy://' + +# Send email to the console by default +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Or send them to /dev/null +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + +# django-mailer uses a different settings attribute +MAILER_EMAIL_BACKEND = EMAIL_BACKEND + # Configure these for your outgoing email host # EMAIL_HOST = 'smtp.my-company.com' # EMAIL_PORT = 25 # EMAIL_HOST_USER = 'djangomail' # EMAIL_HOST_PASSWORD = 'top-secret!' + OPENSTACK_ADMIN_TOKEN = "999888777666" -OPENSTACK_KEYSTONE_URL = "http://localhost:8080/v2.0/" +OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0/" OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" +# NOTE(tres): Replace all of this nonsense once Swift+Keystone +# is stable. +# SWIFT_AUTHURL = 'http://localhost:8080/auth/v1.0' +# SWIFT_ACCOUNT = 'test' +# SWIFT_USER = 'tester' +# SWIFT_PASS = 'testing' + # If you have external monitoring links EXTERNAL_MONITORING = [ ['Nagios','http://foo.com'], @@ -58,10 +75,6 @@ EXTERNAL_MONITORING = [ # 'handlers': ['null'], # 'propagate': False, # }, -# 'boto': { -# 'handlers': ['null'], -# 'propagate': False, -# }, # 'django_openstack': { # 'handlers': ['null'], # 'propagate': False, diff --git a/openstack-dashboard/media/dashboard/css/openstack.css b/openstack-dashboard/media/dashboard/css/openstack.css index 70021e1a5..3abdddaa2 100644 --- a/openstack-dashboard/media/dashboard/css/openstack.css +++ b/openstack-dashboard/media/dashboard/css/openstack.css @@ -1,7 +1,7 @@ @import url("reset.css"); /*================== - === Misc/General + === Misc/General ==================*/ body { @@ -60,7 +60,7 @@ p .ui-icon { /*================== - === Header + === Header ==================*/ #header { @@ -96,7 +96,7 @@ p .ui-icon { /*================== - === Content + === Content ==================*/ #content_wrap { @@ -483,7 +483,7 @@ input.delete, input.detach { } #right_content td.image_location { - text-align: left; + text-align: left; } h3.image_list_heading { @@ -505,7 +505,7 @@ h3.image_list_heading { td.detail_wrapper { padding: 0 !important; text-align: left !important; - + } div.image_detail, div.instance_detail { background: url(/media/dashboard/img/image_detail.png) top left no-repeat; @@ -727,20 +727,19 @@ div.image_detail, div.instance_detail { border-bottom: none; border-top: none; } -#projects .project a.manage_link:hover { +#projects .project a.manage_link:hover { background-color: #f1f1f1 !important; border-color: #b7b7b7; } #projects .project.active a.manage_link { background-color: #5393c9 !important; border-color: #4c83af;} -#projects .project.active a.manage_link:hover { +#projects .project.active a.manage_link:hover { background-color: #80afd6 !important; border-color: #b7b7b7; } - /* Footer */ -#footer { +#footer { clear: both; margin-bottom: 50px; } diff --git a/openstack-dashboard/media/dashboard/css/style.css b/openstack-dashboard/media/dashboard/css/style.css index 77ba5e9d5..23e600aff 100644 --- a/openstack-dashboard/media/dashboard/css/style.css +++ b/openstack-dashboard/media/dashboard/css/style.css @@ -48,7 +48,7 @@ ul, li { h1 { float: left; - + } h1 a{ @@ -93,7 +93,7 @@ border: 1px solid #cbe0e8; z-index: 500; color: #8eacb7; text-transform: uppercase; - font-size: 12px; + font-size: 12px; position: relative; display: block; float: left; @@ -132,7 +132,7 @@ small { } #nav li, #nav li a{ - float: left; + float: left; } #nav li a.active { @@ -172,7 +172,7 @@ small { float: right; height: 400px; margin-bottom: -400px; - + } #login_btn { @@ -219,7 +219,7 @@ small { #standalone #login { float:left; margin: 0 !important; - -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.2); + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.2); } #standalone #login_wrapper .status_box { @@ -333,6 +333,10 @@ input[readonly="readonly"] { float: left; } +.empty-sidebar { + height: 400px; +} + #main { float: left; width: 735px; @@ -357,8 +361,9 @@ input[readonly="readonly"] { } - - +.form-filter { + float: right; +} .search { @@ -605,7 +610,7 @@ table td.name{ } table td.name:hover { - overflow: auto; + overflow: auto; z-index: 999; } @@ -923,7 +928,7 @@ ol { ol li { display: block; - float: left; + float: left; position: relative; margin-right: -28px; } @@ -1106,13 +1111,13 @@ ol li.first a { -webkit-transition-property: background; -webkit-transition-duration: 0.2s; text-shadow: #62935c 0 -1px 0; - background: #abc1a8; - + background: #abc1a8; + } #main a.action_link:hover { - text-decoration: none; - background: #83bf7a; + text-decoration: none; + background: #83bf7a; } .dash_block { @@ -1195,23 +1200,23 @@ ol li.first a { margin-bottom: 25px; } -.status_box.info, .stat_box.info { +.status_box.info { background-color: #e8f8ff; border-color: #9ac7dc; color: #7ab6c5; } -.status_box.success { +.status_box.success { background-color: #e9ffe8; border-color: #9edd9b; color: #7ec67b; } -.status_box.warning { +.status_box.warning { background-color: #ffffe8; border-color: #ffe37b; color: #d1b12d; } -.status_box.error { +.status_box.error { background-color: #ffdbdc; border-color: #ff9a99; color: #ff8080; @@ -1222,7 +1227,7 @@ ol li.first a { float: left; padding: 20px; min-width: 120px; - + } .status_box.info h2 { color: #2a7380; border-color: #9ac7dc; } @@ -1369,18 +1374,18 @@ a.refresh:hover { -.stat_box.good { +.stat_box.good { background-color: #e9ffe8; border-color: #9edd9b; color: #7ec67b; } -.stat_box.medium { +.stat_box.medium { background-color: #ffffe8; border-color: #ffe37b; color: #eada83; } -.stat_box.bad { +.stat_box.bad { background-color: #ffdbdc; border-color: #ff9a99; color: #ff8080; @@ -1419,7 +1424,7 @@ a.refresh:hover { border-radius: 5px; } -.stat_box.small { +.stat_box.small { width: 359px; -webkit-border-radius: 0; -moz-border-radius: 0; @@ -1459,7 +1464,7 @@ a.refresh:hover { } -.stat_box p.avail span, .stat_box p.used span { +.stat_box p.avail span, .stat_box p.used span { font-size: 11px; text-transform: uppercase; } @@ -1715,3 +1720,17 @@ li.title h4{ bottom: 0; margin-bottom: -24px; } + +.container-label { + font-weight: bold; + font-size: 1.22em; + margin-left: 10px; + margin-bottom: 10px; + float: left; +} + +.container-name { + float: left; + margin-left: 5px; +} + diff --git a/openstack-dashboard/tools/install_venv.py b/openstack-dashboard/tools/install_venv.py index 6b03d485c..f2ce797ed 100644 --- a/openstack-dashboard/tools/install_venv.py +++ b/openstack-dashboard/tools/install_venv.py @@ -1,10 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the +# Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # -# Copyright 2010 OpenStack, LLC +# Copyright 2011 OpenStack, LLC +# +# Copyright 2011 Fourth Paradigm Development, Inc. # # 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 diff --git a/openstack-dashboard/tools/pip-requires b/openstack-dashboard/tools/pip-requires index 97cbf52f0..b248decb7 100644 --- a/openstack-dashboard/tools/pip-requires +++ b/openstack-dashboard/tools/pip-requires @@ -1,9 +1,20 @@ -boto==1.9b +nose==1.0.0 Django==1.3 +django-nose==0.1.2 +django-mailer django-registration==0.7 nova-adminclient +python-cloudfiles python-dateutil +routes +webob +sqlalchemy +paste +PasteDeploy +sqlalchemy-migrate +eventlet --e bzr+https://launchpad.net/glance#egg=glance +bzr+https://launchpad.net/glance#egg=glance -e git://github.com/jacobian/openstack.compute.git#egg=openstack -e git://github.com/cloudbuilders/openstackx.git#egg=openstackx +