Re-architects the OpenStack Dashboard for modularity and extensibility.

Implements blueprint extensible-architecture.
Implements blueprint improve-dev-documentation.
Implements blueprint gettext-everywhere.
Implements blueprint sphinx-docs.

Complete re-architecture of the dashboard to transform it from a standalone django-openstack app to a Horizon framework for building dashboards. See the docs for more information.

Incidentally fixes the following bugs:

Fixes bug 845868 -- no PEP8 violations.
Fixes bug 766096 -- the dashboard can now be installed at any arbitrary URL.
Fixes bug 879111 -- tenant id is now controlled solely by the tenant switcher, not the url (which was disregarded anyway)
Fixes bug 794754 -- output of venv installation is considerably reduced.

Due to the scale and scope of this patch I recommend reviewing it on github: https://github.com/gabrielhurley/horizon/tree/extensible_architecture

Change-Id: I8e63f7ea235f904247df40c33cb66338d973df9e
This commit is contained in:
Gabriel Hurley 2011-10-31 11:31:05 -07:00
parent 91ceb59bae
commit 9742842795
403 changed files with 21536 additions and 19149 deletions

View File

@ -1,11 +1,11 @@
django-openstack/.installed.cfg
django-openstack/bin
django-openstack/develop-eggs/
django-openstack/downloads/
django-openstack/eggs/
django-openstack/parts/
django-openstack/src/django_nova.egg-info
django-openstack/src/django_openstack.egg-info
horizon/.installed.cfg
horizon/bin
horizon/develop-eggs/
horizon/downloads/
horizon/eggs/
horizon/parts/
horizon/src/django_nova.egg-info
horizon/src/django_openstack.egg-info
django-nova-syspanel/src/django_nova_syspanel.egg-info
openstack-dashboard/.dashboard-venv
openstack-dashboard/local/dashboard_openstack.sqlite3

23
.gitignore vendored
View File

@ -5,19 +5,20 @@ coverage.xml
pep8.txt
pylint.txt
reports
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/django_nova.egg-info
django-openstack/django_openstack.egg-info
horizon/.installed.cfg
horizon/bin
horizon/develop-eggs/
horizon/downloads/
horizon/eggs/
horizon/htmlcov
horizon/launchpad
horizon/parts/
horizon/django_nova.egg-info
horizon/horizon.egg-info
horizon/django_openstack.egg-info
django-nova-syspanel/src/django_nova_syspanel.egg-info
openstack-dashboard/.dashboard-venv
openstack-dashboard/local/dashboard_openstack.sqlite3
openstack-dashboard/local/local_settings.py
build/
doc/source/sourcecode
docs/source/sourcecode

34
README
View File

@ -4,14 +4,14 @@ OpenStack Dashboard (Horizon)
The OpenStack Dashboard is a Django based reference implementation of a web
based management interface for OpenStack.
It is based on django-openstack, which is designed to be a generic Django
module that can be re-used in other sites.
It is based on the ``horizon`` module, which is designed to be a generic Django
app that can be re-used in other projects.
For more information about how to get started with the OpenStack Dashboard,
view the README file in the openstack-dashboard folder.
For more information about working directly with django-openstack, see the
README file in the django-openstack folder.
For more information about working directly with ``horizon``, see the
README file in the ``horizon`` folder.
For release management:
@ -29,21 +29,21 @@ Project Structure and Testing:
------------------------------
This project is a bit different from other Openstack projects in that it has
two very distinct components underneath it: django-openstack, and
openstack-dashboard.
two very distinct components underneath it: ``horizon``, and
``openstack-dashboard``.
django-openstack holds the generic libraries and components that can be
used in any Django project. In testing, this component is set up with
buildout (see run_tests.sh), and any dependencies that get added need to
be added to the django-openstack/buildout.cfg file.
The ``horizon`` directory holds the generic libraries and components that can
be used in any Django project. In testing, this component is set up with
buildout (see ``run_tests.sh``), and any dependencies that get added need to
be added to the ``horizon/buildout.cfg`` file.
openstack-dashboard is a reference django project that uses django-openstack
and is built with a virtualenv and tested through that environment. If
depdendencies are added that the reference django project needs, they
should be added to openstack-dashboard/tools/pip-requires.
The ``openstack-dashboard`` directory contains a reference Django project that
uses ``horizon`` and is built with a virtualenv and tested through that
environment. If dependencies are added that ``openstack-dashboard`` requires
they should be added to ``openstack-dashboard/tools/pip-requires``.
The run_tests.sh script invokes tests and analysis on both of these
components in it's process, and is what Jenkins uses to verify the
The ``run_tests.sh`` script invokes tests and analyses on both of these
components in its process, and is what Jenkins uses to verify the
stability of the project.
To run the tests::
@ -55,7 +55,7 @@ Building Contributor Documentation
This documentation is written by contributors, for contributors.
The source is maintained in the `doc/source` folder using
The source is maintained in the ``docs/source`` folder using
`reStructuredText`_ and built by `Sphinx`_
.. _reStructuredText: http://docutils.sourceforge.net/rst.html

View File

@ -1,56 +0,0 @@
Django-OpenStack
---------------------
The Django-OpenStack project is a Django module that is used to provide web based
interactions with an OpenStack cloud.
There is a reference implementation that uses this module located at:
http://launchpad.net/horizon
It is highly recommended that you make use of this reference implementation
so that changes you make can be visualized effectively and are consistent.
Using this reference implementation as a development environment will greatly
simplify development of the django-openstack module.
Of course, if you are developing your own Django site using django-openstack, then
you can disregard this advice.
Getting Started
---------------
Django-OpenStack uses Buildout (http://www.buildout.org/) to manage local
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
This will install all the dependencies of django-openstack and provide some useful
scripts in the bin/ directory:
bin/python provides a python shell for the current buildout.
bin/django provides django functions for the current buildout.
You should now be able to run unit tests as follows:
$ bin/django test
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

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +0,0 @@
# 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 Nebula, 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
def tenants(request):
if not request.user or not request.user.is_authenticated():
return {}
try:
return {'tenants': api.tenant_list_for_token(request,
request.user.token)}
except api_exceptions.BadRequest, e:
messages.error(request, _("Unable to retrieve tenant list from\
keystone: %s") % e.message)
return {'tenants': []}
def object_store(request):
catalog = getattr(request.user, 'service_catalog', [])
object_store = catalog and api.get_service_from_catalog(catalog,
'object-store')
return {'object_store_configured': object_store}
def quantum(request):
return {'quantum_configured': settings.QUANTUM_ENABLED}

View File

@ -1,118 +0,0 @@
# 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 Nebula, 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 *
SECURITY_GROUPS = r'^(?P<tenant_id>[^/]+)/security_groups/' \
'(?P<security_group_id>[^/]+)/%s$'
INSTANCES = r'^(?P<tenant_id>[^/]+)/instances/(?P<instance_id>[^/]+)/%s$'
IMAGES = r'^(?P<tenant_id>[^/]+)/images/(?P<image_id>[^/]+)/%s$'
KEYPAIRS = r'^(?P<tenant_id>[^/]+)/keypairs/%s$'
SNAPSHOTS = r'^(?P<tenant_id>[^/]+)/snapshots/(?P<instance_id>[^/]+)/%s$'
VOLUMES = r'^(?P<tenant_id>[^/]+)/volumes/(?P<volume_id>[^/]+)/%s$'
CONTAINERS = r'^(?P<tenant_id>[^/]+)/containers/%s$'
FLOATING_IPS = r'^(?P<tenant_id>[^/]+)/floating_ips/(?P<ip_id>[^/]+)/%s$'
OBJECTS = r'^(?P<tenant_id>[^/]+)/containers/(?P<container_name>[^/]+)/%s$'
NETWORKS = r'^(?P<tenant_id>[^/]+)/networks/%s$'
PORTS = r'^(?P<tenant_id>[^/]+)/networks/(?P<network_id>[^/]+)/ports/%s$'
urlpatterns = patterns('django_openstack.dash.views.instances',
url(r'^(?P<tenant_id>[^/]+)/$', 'usage', name='dash_usage'),
url(r'^(?P<tenant_id>[^/]+)/instances/$', 'index', name='dash_instances'),
url(r'^(?P<tenant_id>[^/]+)/instances/refresh$', 'refresh',
name='dash_instances_refresh'),
url(INSTANCES % 'detail', 'detail', name='dash_instances_detail'),
url(INSTANCES % 'console', 'console', name='dash_instances_console'),
url(INSTANCES % 'vnc', 'vnc', name='dash_instances_vnc'),
url(INSTANCES % 'update', 'update', name='dash_instances_update'),
)
urlpatterns += patterns('django_openstack.dash.views.security_groups',
url(r'^(?P<tenant_id>[^/]+)/security_groups/$', 'index',
name='dash_security_groups'),
url(r'^(?P<tenant_id>[^/]+)/security_groups/create$', 'create',
name='dash_security_groups_create'),
url(SECURITY_GROUPS % 'edit_rules', 'edit_rules',
name='dash_security_groups_edit_rules'),
)
urlpatterns += patterns('django_openstack.dash.views.images',
url(r'^(?P<tenant_id>[^/]+)/images/$', 'index', name='dash_images'),
url(IMAGES % 'launch', 'launch', name='dash_images_launch'),
url(IMAGES % 'update', 'update', name='dash_images_update'),
)
urlpatterns += patterns('django_openstack.dash.views.keypairs',
url(r'^(?P<tenant_id>[^/]+)/keypairs/$', 'index', name='dash_keypairs'),
url(KEYPAIRS % 'create', 'create', name='dash_keypairs_create'),
url(KEYPAIRS % 'import', 'import_keypair', name='dash_keypairs_import'),
)
urlpatterns += patterns('django_openstack.dash.views.floating_ips',
url(r'^(?P<tenant_id>[^/]+)/floating_ips/$', 'index',
name='dash_floating_ips'),
url(FLOATING_IPS % 'associate', 'associate',
name='dash_floating_ips_associate'),
url(FLOATING_IPS % 'disassociate', 'disassociate',
name='dash_floating_ips_disassociate'),
)
urlpatterns += patterns('django_openstack.dash.views.snapshots',
url(r'^(?P<tenant_id>[^/]+)/snapshots/$', 'index', name='dash_snapshots'),
url(SNAPSHOTS % 'create', 'create', name='dash_snapshots_create'),
)
urlpatterns += patterns('django_openstack.dash.views.volumes',
url(r'^(?P<tenant_id>[^/]+)/volumes/$', 'index', name='dash_volumes'),
url(r'^(?P<tenant_id>[^/]+)/volumes/create', 'create',
name='dash_volumes_create'),
url(VOLUMES % 'attach', 'attach', name='dash_volumes_attach'),
url(VOLUMES % 'detail', 'detail', name='dash_volumes_detail'),
)
# 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<object_name>[^/]+)/copy',
'copy', name='dash_object_copy'),
url(OBJECTS % '(?P<object_name>[^/]+)/download',
'download', name='dash_objects_download'),
)
urlpatterns += patterns('django_openstack.dash.views.networks',
url(r'^(?P<tenant_id>[^/]+)/networks/$', 'index', name='dash_networks'),
url(NETWORKS % 'create', 'create', name='dash_network_create'),
url(NETWORKS % '(?P<network_id>[^/]+)/detail', 'detail',
name='dash_networks_detail'),
url(NETWORKS % '(?P<network_id>[^/]+)/rename', 'rename',
name='dash_network_rename'),
)
urlpatterns += patterns('django_openstack.dash.views.ports',
url(PORTS % 'create', 'create', name='dash_ports_create'),
url(PORTS % '(?P<port_id>[^/]+)/attach', 'attach',
name='dash_ports_attach'),
)

View File

@ -1,95 +0,0 @@
# 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 Nebula, 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.utils.translation import ugettext as _
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(request, data['container_name'])
except ContainerNotEmpty, e:
messages.error(request,
_('Unable to delete non-empty container: %s') %
data['container_name'])
LOG.exception('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(request, data['name'])
messages.success(request, _("Container was successfully created."))
return shortcuts.redirect("dash_containers", request.user.tenant_id)
@login_required
def index(request, tenant_id):
marker = request.GET.get('marker', None)
delete_form, handled = DeleteContainer.maybe_handle(request)
if handled:
return handled
containers = api.swift_get_containers(request, marker=marker)
return shortcuts.render_to_response(
'django_openstack/dash/containers/index.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(
'django_openstack/dash/containers/create.html', {
'create_form': form,
}, context_instance=template.RequestContext(request))

View File

@ -1,252 +0,0 @@
# 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 Nebula, 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 api.quantum_api(request) networks.
"""
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.utils import simplejson
from django.utils.translation import ugettext as _
from django_openstack import forms
from django_openstack import api
from django_openstack.dash.views.ports import DeletePort
from django_openstack.dash.views.ports import DetachPort
from django_openstack.dash.views.ports import TogglePort
import warnings
LOG = logging.getLogger('django_openstack.dash.views.networks')
class CreateNetwork(forms.SelfHandlingForm):
name = forms.CharField(required=True, label=_("Network Name"))
def handle(self, request, data):
network_name = data['name']
try:
LOG.info('Creating network %s ' % network_name)
send_data = {'network': {'name': '%s' % network_name}}
api.quantum_create_network(request, send_data)
except Exception, e:
messages.error(request,
_('Unable to create network %(network)s: %(msg)s') %
{"network": network_name, "msg": e.message})
return shortcuts.redirect(request.build_absolute_uri())
else:
msg = _('Network %s has been created.') % network_name
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect('dash_networks',
tenant_id=request.user.tenant_id)
class DeleteNetwork(forms.SelfHandlingForm):
network = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
LOG.info('Deleting network %s ' % data['network'])
api.quantum_delete_network(request, data['network'])
except Exception, e:
messages.error(request,
_('Unable to delete network %(network)s: %(msg)s') %
{"network": data['network'], "msg": e.message})
else:
msg = _('Network %s has been deleted.') % data['network']
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class RenameNetwork(forms.SelfHandlingForm):
network = forms.CharField(widget=forms.HiddenInput())
new_name = forms.CharField(required=True)
def handle(self, request, data):
try:
LOG.info('Renaming network %s to %s' %
(data['network'], data['new_name']))
send_data = {'network': {'name': '%s' % data['new_name']}}
api.quantum_update_network(request, data['network'], send_data)
except Exception, e:
messages.error(request,
_('Unable to rename network %(network)s: %(msg)s') %
{"network": data['network'], "msg": e.message})
else:
msg = _('Network %(net)s has been renamed to %(new_name)s.') % {
"net": data['network'], "new_name": data['new_name']}
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
@login_required
def index(request, tenant_id):
delete_form, delete_handled = DeleteNetwork.maybe_handle(request)
networks = []
instances = []
try:
networks_list = api.quantum_list_networks(request)
details = []
for network in networks_list['networks']:
net_stats = _calc_network_stats(request, tenant_id, network['id'])
# Get network details like name and id
details = api.quantum_network_details(request, network['id'])
networks.append({
'name': details['network']['name'],
'id': network['id'],
'total': net_stats['total'],
'available': net_stats['available'],
'used': net_stats['used'],
'tenant': tenant_id
})
except Exception, e:
messages.error(request,
_('Unable to get network list: %s') % e.message)
return shortcuts.render_to_response(
'django_openstack/dash/networks/index.html', {
'networks': networks,
'delete_form': delete_form,
}, context_instance=template.RequestContext(request))
@login_required
def create(request, tenant_id):
network_form, handled = CreateNetwork.maybe_handle(request)
if handled:
return shortcuts.redirect('dash_networks', request.user.tenant_id)
return shortcuts.render_to_response(
'django_openstack/dash/networks/create.html', {
'network_form': network_form
}, context_instance=template.RequestContext(request))
@login_required
def detail(request, tenant_id, network_id):
delete_port_form, delete_handled = DeletePort.maybe_handle(request)
detach_port_form, detach_handled = DetachPort.maybe_handle(request)
toggle_port_form, port_toggle_handled = TogglePort.maybe_handle(request)
network = {}
try:
network_details = api.quantum_network_details(request, network_id)
network['name'] = network_details['network']['name']
network['id'] = network_id
network['ports'] = _get_port_states(request, tenant_id, network_id)
except Exception, e:
messages.error(request,
_('Unable to get network details: %s') % e.message)
return shortcuts.render_to_response(
'django_openstack/dash/networks/detail.html', {
'network': network,
'tenant': tenant_id,
'delete_port_form': delete_port_form,
'detach_port_form': detach_port_form,
'toggle_port_form': toggle_port_form
}, context_instance=template.RequestContext(request))
@login_required
def rename(request, tenant_id, network_id):
rename_form, handled = RenameNetwork.maybe_handle(request)
network_details = api.quantum_network_details(request, network_id)
if handled:
return shortcuts.redirect('dash_networks', request.user.tenant_id)
return shortcuts.render_to_response(
'django_openstack/dash/networks/rename.html', {
'network': network_details,
'rename_form': rename_form
}, context_instance=template.RequestContext(request))
def _get_port_states(request, tenant_id, network_id):
"""
Helper method to find port states for a network
"""
network_ports = []
# Get all vifs for comparison with port attachments
vifs = api.get_vif_ids(request)
# Get all ports on this network
ports = api.quantum_list_ports(request, network_id)
for port in ports['ports']:
port_details = api.quantum_port_details(request,
network_id, port['id'])
# Get port attachments
port_attachment = api.quantum_port_attachment(request,
network_id, port['id'])
# Find instance the attachment belongs to
connected_instance = None
if port_attachment['attachment']:
for vif in vifs:
if str(vif['id']) == str(port_attachment['attachment']['id']):
connected_instance = vif['instance_name']
break
network_ports.append({
'id': port_details['port']['id'],
'state': port_details['port']['state'],
'attachment': port_attachment['attachment'],
'instance': connected_instance
})
return network_ports
def _calc_network_stats(request, tenant_id, network_id):
"""
Helper method to calculate statistics for a network
"""
# Get all ports statistics for the network
total = 0
available = 0
used = 0
ports = api.quantum_list_ports(request, network_id)
for port in ports['ports']:
total += 1
# Get port attachment
port_attachment = api.quantum_port_attachment(request,
network_id, port['id'])
if port_attachment['attachment']:
used += 1
else:
available += 1
return {'total': total, 'used': used, 'available': available}

View File

@ -1,42 +0,0 @@
# 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 CRS4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Simple decorator container for general purpose
"""
from django.shortcuts import redirect
import logging
LOG = logging.getLogger('django_openstack.syspanel')
def enforce_admin_access(fn):
""" Preserve unauthorized bypass typing directly the URL and redirects to
the overview dash page """
def dec(*args, **kwargs):
if args[0].user.is_admin():
return fn(*args, **kwargs)
else:
LOG.warn('Redirecting user "%s" from syspanel to dash ( %s )' %
(args[0].user.username, fn.__name__))
return redirect('dash_overview')
return dec

View File

@ -1,8 +0,0 @@
""" Standardized exception classes for the OpenStack Dashboard. """
from novaclient import exceptions as nova_exceptions
class Unauthorized(nova_exceptions.Unauthorized):
""" A wrapper around novaclient's Unauthorized exception. """
pass

View File

@ -1,50 +0,0 @@
# 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 Nebula, 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 django.dispatch
from django.dispatch import receiver
dash_modules_ping = django.dispatch.Signal()
dash_modules_urls = django.dispatch.Signal()
def dash_modules_detect():
"""
Sends a pinging signal to the app, all listening modules will reply with
items for the sidebar.
The response is a tuple of the Signal object instance, and a dictionary.
The values within the dictionary containing links and a title which should
be added to the sidebar navigation.
Example: (<dash_apps_ping>,
{'title': 'Nixon',
'links': [{'url':'/syspanel/nixon/google',
'text':'Google', 'active_text': 'google'}],
'type': syspanel})
"""
return dash_modules_ping.send(sender=dash_modules_ping)
def dash_app_setup_urls():
"""
Adds urls from modules
"""
return dash_modules_urls.send(sender=dash_modules_urls)

View File

@ -1,78 +0,0 @@
# 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 Nebula, 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'^instances/(?P<instance_id>[^/]+)/%s$'
IMAGES = r'^images/(?P<image_id>[^/]+)/%s$'
USERS = r'^users/(?P<user_id>[^/]+)/%s$'
TENANTS = r'^tenants/(?P<tenant_id>[^/]+)/%s$'
urlpatterns = patterns('django_openstack.syspanel.views.instances',
url(r'^usage/(?P<tenant_id>[^/]+)$', 'tenant_usage',
name='syspanel_tenant_usage'),
url(r'^instances/$', 'index', name='syspanel_instances'),
url(r'^instances/refresh$', 'refresh', name='syspanel_instances_refresh'),
url(INSTANCES % 'detail', 'detail', name='syspanel_instances_detail'),
# NOTE(termie): currently just using the 'dash' versions
#url(INSTANCES % 'console', 'console', name='syspanel_instances_console'),
#url(INSTANCES % 'vnc', 'vnc', name='syspanel_instances_vnc'),
)
urlpatterns += patterns('django_openstack.syspanel.views.images',
url(r'^images/$', 'index', name='syspanel_images'),
url(IMAGES % 'update', 'update', name='syspanel_images_update'),
#url(INSTANCES % 'vnc', 'vnc', name='syspanel_instances_vnc'),
)
urlpatterns += patterns('django_openstack.syspanel.views.quotas',
url(r'^quotas/$', 'index', name='syspanel_quotas'),
)
urlpatterns += patterns('django_openstack.syspanel.views.flavors',
url(r'^flavors/$', 'index', name='syspanel_flavors'),
url(r'^flavors/create/$', 'create', name='syspanel_flavors_create'),
)
urlpatterns += patterns('django_openstack.syspanel.views.users',
url(r'^users/$', 'index', name='syspanel_users'),
url(USERS % 'update', 'update', name='syspanel_users_update'),
url(r'^users/create$', 'create', name='syspanel_users_create'),
)
urlpatterns += patterns('django_openstack.syspanel.views.services',
url(r'^services/$', 'index', name='syspanel_services'),
)
urlpatterns += patterns('django_openstack.syspanel.views.tenants',
url(r'^tenants/$', 'index', name='syspanel_tenants'),
url(r'^tenants/create$', 'create', name='syspanel_tenants_create'),
url(TENANTS % 'update', 'update', name='syspanel_tenant_update'),
url(TENANTS % 'users', 'users', name='syspanel_tenant_users'),
url(TENANTS % 'quotas', 'quotas', name='syspanel_tenant_quotas'),
)

View File

@ -1,17 +0,0 @@
{%load i18n%}
<form action="" method="post">
{% csrf_token %}
<fieldset>
{% for hidden in form.hidden_fields %}
{{hidden}}
{% endfor %}
{% for field in form.visible_fields %}
{{field.label_tag}}
{{field.errors}}
{{field}}
{% endfor %}
{% block submit %}
<input type="submit" value="{%trans "Login"%}" />
{% endblock %}
</fieldset>
</form>

View File

@ -1,9 +0,0 @@
{%load i18n%}
<form id="form_reboot_{{instance.id}}" class="form-reboot" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{hidden}}
{% endfor %}
<input name="instance" type="hidden" value="{{instance.id}}" />
<input id="reboot_{{instance.id}}" class="reboot" title="Instance: {{instance.name}}" type="submit" value="{%trans "Reboot"%}" />
</form>

View File

@ -1,9 +0,0 @@
{%load i18n%}
<form id="form_terminate_{{instance.id}}" class="form-terminate" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{hidden}}
{% endfor %}
<input name="instance" type="hidden" value="{{instance.id}}" />
<input id="terminate_{{instance.id}}" class="terminate" title="{{instance.name}}" type="submit" value="{%trans "Terminate"%}" />
</form>

View File

@ -1,28 +0,0 @@
{% load sidebar_modules %}
{%load i18n%}
<div id='sidebar'>
<h3>{% trans "Manage Compute"%}</h3>
<ul class='sub_nav'>
<li><a {% if current_sidebar == "overview" %} class="active" {% endif %} href="{% url dash_overview %}">{% trans "Overview"%}</a></li>
<li><a {% if current_sidebar == "instances" %} class="active" {% endif %} href="{% url dash_instances request.user.tenant_id %}">{% trans "Instances"%}</a></li>
<li><a {% if current_sidebar == "images" %} class="active" {% endif %} href="{% url dash_images request.user.tenant_id %}">{% trans "Images"%}</a></li>
<li><a {% if current_sidebar == "snapshots" %} class="active" {% endif %} href="{% url dash_snapshots request.user.tenant_id %}">{% trans "Snapshots"%}</a></li>
<li><a {% if current_sidebar == "keypairs" %} class="active" {% endif %} href="{% url dash_keypairs request.user.tenant_id %}">{% trans "Keypairs"%}</a></li>
<li><a {% if current_sidebar == "floatingips" %} class="active" {% endif %} href="{% url dash_floating_ips request.user.tenant_id %}">{% trans "Floating IPs"%}</a></li>
<li><a {% if current_sidebar == "security_groups" %} class="active" {% endif %} href="{% url dash_security_groups request.user.tenant_id %}">{% trans "Security Groups"%}</a></li>
<li><a {% if current_sidebar == "volumes" %} class="active" {% endif %} href="{% url dash_volumes request.user.tenant_id %}">{% trans "Volumes"%}</a></li>
{% if quantum_configured %}
<li><a {% if current_sidebar == "networks" %} class="active" {% endif %} href="{% url dash_networks request.user.tenant_id %}">{% trans "Networks"%}</a></li>
{% endif %}
</ul>
{% if object_store_configured %}
<h3>{% trans "Manage Object Store"%}</h3>
<ul class='sub_nav'>
<li><a {% if current_sidebar == "containers" %} class="active" {% endif %} href="{% url dash_containers request.user.tenant_id %}">{% trans "Containers"%}</a></li>
</ul>
{% endif %}
{% dash_sidebar_modules request %}
</div>

View File

@ -1,19 +0,0 @@
{% extends 'django_openstack/dash/base.html' %}
{%load i18n%}
{% block sidebar %}
{% with current_sidebar="containers" %}
{{block.super}}
{% endwith %}
{% endblock %}
{% block page_header %}
{% url dash_images request.user.tenant_id as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "django_openstack/common/_page_header.html" with title=_("Containers") refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block dash_main %}
{% include 'django_openstack/dash/containers/_list.html' %}
<a class="action_link large-rounded" href="{% url dash_containers_create request.user.tenant_id %}">{% trans "Create New Container"%} &gt;&gt;</a>
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends 'django_openstack/dash/base.html' %}
{%load i18n%}
{% block sidebar %}
{% with current_sidebar="networks" %}
{{block.super}}
{% endwith %}
{% endblock %}
{% block page_header %}
{% url dash_networks_detail request.user.tenant_id network.id as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "django_openstack/common/_page_header.html" with title=network.name refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block breadcrumbs %}
<a href="{% url dash_networks tenant %}">Networks</a>&nbsp;&raquo;&nbsp;
<a href="{% url dash_networks_detail tenant network.id %}">{{network.name}}</a>
{% endblock %}
{% block dash_main %}
{% if network.ports %}
{% include 'django_openstack/dash/networks/_detail.html' %}
<a id="network_create_link" class="action_link large-rounded" href="{% url dash_ports_create request.user.tenant_id network.id %}">{% trans "Create Ports"%}</a>
{% else %}
<div class="message_box info">
<h2>{% trans "Info"%}</h2>
<p>{% trans "There are currently no ports in this network."%} <a href="{% url dash_ports_create request.user.tenant_id network.id %}">{% trans "Create Ports"%} &gt;&gt;</a></p>
</div>
{% endif %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% load sidebar_modules %}
{%load i18n%}
<div id='sidebar'>
<h3>{% trans "System Panel"%}</h3>
<ul class='sub_nav'>
<li><a {% if current_sidebar == "overview" %} class="active" {% endif %} href="{% url syspanel_overview %}">{% trans "Overview"%}</a></li>
<li><a {% if current_sidebar == "services" %} class="active" {% endif %} href="{% url syspanel_services %}">{% trans "Services"%}</a></li>
<li><a {% if current_sidebar == "instances" %} class="active" {% endif %} href="{% url syspanel_instances %}">{% trans "Instances"%}</a></li>
<li><a {% if current_sidebar == "flavors" %} class="active" {% endif %} href="{% url syspanel_flavors %}">{% trans "Flavors"%}</a></li>
<li><a {% if current_sidebar == "images" %} class="active" {% endif %} href="{% url syspanel_images %}">{% trans "Images"%}</a></li>
<li><a {% if current_sidebar == "tenants" %} class="active" {% endif %} href="{% url syspanel_tenants %}">{% trans "Tenants"%}</a></li>
<li><a {% if current_sidebar == "users" %} class="active" {% endif %} href="{% url syspanel_users %}">{% trans "Users"%}</a></li>
<li><a {% if current_sidebar == "quotas" %} class="active" {% endif %} href="{% url syspanel_quotas %}">{% trans "Quotas"%}</a></li>
</ul>
{% syspanel_sidebar_modules request %}
</div>

View File

@ -1,19 +0,0 @@
{% extends 'django_openstack/syspanel/base.html' %}
{%load i18n%}
{% block sidebar %}
{% with current_sidebar="flavors" %}
{{block.super}}
{% endwith %}
{% endblock %}
{% block page_header %}
{% url syspanel_flavors as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "django_openstack/common/_page_header.html" with title=_("Flavors") refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block syspanel_main %}
{% include "django_openstack/syspanel/flavors/_list.html" %}
<a id="flavor_create_link" class="action_link large-rounded" href="{% url syspanel_flavors_create %}">{% trans "Create New Flavor"%}</a>
{% endblock %}

View File

@ -1,46 +0,0 @@
# 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 Nebula, 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_openstack import signals
register = template.Library()
@register.inclusion_tag('django_openstack/common/_sidebar_module.html')
def dash_sidebar_modules(request):
signals_call = signals.dash_modules_detect()
if signals_call:
return {'modules': [module[1] for module in signals_call
if module[1]['type'] == "dash"],
'request': request}
else:
return {}
@register.inclusion_tag('django_openstack/common/_sidebar_module.html')
def syspanel_sidebar_modules(request):
signals_call = signals.dash_modules_detect()
if signals_call:
return {'modules': [module[1] for module in signals_call
if module[1]['type'] == "syspanel"],
'request': request}
else:
return {}

View File

@ -1,15 +0,0 @@
from django import template
from django.conf import settings
from django.utils import http
register = template.Library()
@register.inclusion_tag('django_openstack/dash/objects/_paging.html')
def object_paging(objects):
marker = None
if objects and not \
len(objects) < getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000):
last_object = objects[-1]
marker = http.urlquote_plus(last_object.name)
return {'marker': marker}

View File

@ -1,99 +0,0 @@
# 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 Nebula, 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 http
from django import test
import mox
from django_openstack.middleware import keystone
class TestCase(test.TestCase):
TEST_STAFF_USER = 'staffUser'
TEST_TENANT = '1'
TEST_TENANT_NAME = 'aTenant'
TEST_TOKEN = 'aToken'
TEST_USER = 'test'
TEST_SERVICE_CATALOG = [{
"endpoints": [{
"adminURL": "http://cdn.admin-nets.local:8774/v1.0",
"region": "RegionOne",
"internalURL": "http://127.0.0.1:8774/v1.0",
"publicURL": "http://cdn.admin-nets.local:8774/v1.0/"
}],
"type": "nova_compat",
"name": "nova_compat"
}, {
"endpoints": [{
"adminURL": "http://nova/novapi/admin",
"region": "RegionOne",
"internalURL": "http://nova/novapi/internal",
"publicURL": "http://nova/novapi/public"
}],
"type": "compute",
"name": "nova"
}, {
"endpoints": [{
"adminURL": "http://glance/glanceapi/admin",
"region": "RegionOne",
"internalURL": "http://glance/glanceapi/internal",
"publicURL": "http://glance/glanceapi/public"
}],
"type": "image",
"name": "glance"
}, {
"endpoints": [{
"adminURL": "http://cdn.admin-nets.local:35357/v2.0",
"region": "RegionOne",
"internalURL": "http://127.0.0.1:5000/v2.0",
"publicURL": "http://cdn.admin-nets.local:5000/v2.0"
}],
"type": "identity",
"name": "identity"
}, {
"endpoints": [{
"adminURL": "http://swift/swiftapi/admin",
"region": "RegionOne",
"internalURL": "http://swift/swiftapi/internal",
"publicURL": "http://swift/swiftapi/public"
}],
"type": "object-store",
"name": "swift"
}]
def setUp(self):
self.mox = mox.Mox()
self._real_get_user_from_request = keystone.get_user_from_request
self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT,
True, self.TEST_SERVICE_CATALOG)
self.request = http.HttpRequest()
keystone.AuthenticationMiddleware().process_request(self.request)
def tearDown(self):
self.mox.UnsetStubs()
keystone.get_user_from_request = self._real_get_user_from_request
def setActiveUser(self, token=None, username=None, tenant_id=None,
is_admin=None, service_catalog=None, tenant_name=None):
keystone.get_user_from_request = \
lambda x: keystone.User(token, username, tenant_id,
is_admin, service_catalog, tenant_name)

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
Intentionally not a python module so that test runner won't find
these broken tests

View File

@ -1,81 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base classes for view based unit tests.
"""
import mox
import nova_adminclient as adminclient
from django import test
from django.conf import settings
from django.contrib.auth import models as auth_models
TEST_PROJECT = 'test'
TEST_USER = 'test'
TEST_REGION = 'test'
class BaseViewTests(test.TestCase):
def setUp(self):
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def assertRedirectsNoFollow(self, response, expected_url):
self.assertEqual(response._headers['location'],
('Location', settings.TESTSERVER + expected_url))
self.assertEqual(response.status_code, 302)
def authenticateTestUser(self):
user = auth_models.User.objects.create_user(TEST_USER,
'test@test.com',
password='test')
login = self.client.login(username=TEST_USER, password='test')
self.failUnless(login, 'Unable to login')
return user
class BaseProjectViewTests(BaseViewTests):
def setUp(self):
super(BaseProjectViewTests, self).setUp()
project = adminclient.ProjectInfo()
project.projectname = TEST_PROJECT
project.projectManagerId = TEST_USER
self.user = self.authenticateTestUser()
self.region = adminclient.RegionInfo(name=TEST_REGION,
endpoint='http://test:8773/')
def create_key_pair_choices(self, key_names):
return [(k, k) for k in key_names]
def create_instance_type_choices(self):
return [('m1.medium', 'm1.medium'),
('m1.large', 'm1.large')]
def create_instance_choices(self, instance_ids):
return [(id, id) for id in instance_ids]
def create_available_volume_choices(self, volumes):
return [(v.id, '%s %s - %dGB' % (v.id, v.displayName, v.size)) \
for v in volumes]

View File

@ -1,70 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for credential views.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django_openstack import models
from django_openstack.nova.tests.base import BaseViewTests
class CredentialViewTests(BaseViewTests):
def test_download_expired_credentials(self):
auth_token = 'expired'
self.mox.StubOutWithMock(models.CredentialsAuthorization,
'get_by_token')
models.CredentialsAuthorization.get_by_token(auth_token) \
.AndReturn(None)
self.mox.ReplayAll()
res = self.client.get(reverse('nova_credentials_authorize',
args=[auth_token]))
self.assertTemplateUsed(res,
'django_openstack/nova/credentials/expired.html')
self.mox.VerifyAll()
def test_download_good_credentials(self):
auth_token = 'good'
creds = models.CredentialsAuthorization()
creds.username = 'test'
creds.project = 'test'
creds.auth_token = auth_token
self.mox.StubOutWithMock(models.CredentialsAuthorization,
'get_by_token')
self.mox.StubOutWithMock(creds, 'get_zip')
models.CredentialsAuthorization.get_by_token(auth_token) \
.AndReturn(creds)
creds.get_zip().AndReturn('zip')
self.mox.ReplayAll()
res = self.client.get(reverse('nova_credentials_authorize',
args=[auth_token]))
self.assertEqual(res.status_code, 200)
self.assertEqual(res['Content-Disposition'],
'attachment; filename=%s-test-test-x509.zip' %
settings.SITE_NAME)
self.assertContains(res, 'zip')
self.mox.VerifyAll()

View File

@ -1,237 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for image views.
"""
import boto.ec2.image
import boto.ec2.instance
import mox
from django.core.urlresolvers import reverse
from django_openstack.nova import forms
from django_openstack.nova import shortcuts
from django_openstack.nova.tests.base import (BaseProjectViewTests,
TEST_PROJECT)
TEST_IMAGE_ID = 'ami_test'
TEST_INSTANCE_ID = 'i-abcdefg'
TEST_KEY = 'foo'
class ImageViewTests(BaseProjectViewTests):
def setUp(self):
self.ami = boto.ec2.image.Image()
self.ami.id = TEST_IMAGE_ID
setattr(self.ami, 'displayName', TEST_IMAGE_ID)
setattr(self.ami, 'description', TEST_IMAGE_ID)
super(ImageViewTests, self).setUp()
def test_index(self):
self.mox.StubOutWithMock(self.project, 'get_images')
self.mox.StubOutWithMock(forms, 'get_key_pair_choices')
self.mox.StubOutWithMock(forms, 'get_instance_type_choices')
self.project.get_images().AndReturn([])
forms.get_key_pair_choices(self.project).AndReturn([])
forms.get_instance_type_choices().AndReturn([])
self.mox.ReplayAll()
res = self.client.get(reverse('nova_images', args=[TEST_PROJECT]))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'django_openstack/nova/images/index.html')
self.assertEqual(len(res.context['image_lists']), 3)
self.mox.VerifyAll()
def test_launch_form(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.StubOutWithMock(forms, 'get_key_pair_choices')
self.mox.StubOutWithMock(forms, 'get_instance_type_choices')
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
forms.get_key_pair_choices(self.project).AndReturn([])
forms.get_instance_type_choices().AndReturn([])
self.mox.ReplayAll()
args = [TEST_PROJECT, TEST_IMAGE_ID]
res = self.client.get(reverse('nova_images_launch', args=args))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res,
'django_openstack/nova/images/launch.html')
self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID)
self.mox.VerifyAll()
def test_launch(self):
instance = boto.ec2.instance.Instance()
instance.id = TEST_INSTANCE_ID
instance.image_id = TEST_IMAGE_ID
reservation = boto.ec2.instance.Reservation()
reservation.instances = [instance]
conn = self.mox.CreateMockAnything()
self.mox.StubOutWithMock(forms, 'get_key_pair_choices')
self.mox.StubOutWithMock(forms, 'get_instance_type_choices')
self.mox.StubOutWithMock(self.project, 'get_openstack_connection')
self.project.get_openstack_connection().AndReturn(conn)
forms.get_key_pair_choices(self.project).AndReturn(
self.create_key_pair_choices([TEST_KEY]))
forms.get_instance_type_choices().AndReturn(
self.create_instance_type_choices())
params = {'addressing_type': 'private',
'UserData': '', 'display_name': u'name',
'MinCount': '1', 'key_name': TEST_KEY,
'MaxCount': '1', 'InstanceType': 'm1.medium',
'ImageId': TEST_IMAGE_ID}
conn.get_object('RunInstances', params, boto.ec2.instance.Reservation,
verb='POST').AndReturn(reservation)
self.mox.ReplayAll()
url = reverse('nova_images_launch', args=[TEST_PROJECT, TEST_IMAGE_ID])
data = {'key_name': TEST_KEY,
'count': '1',
'size': 'm1.medium',
'display_name': 'name',
'user_data': ''}
res = self.client.post(url, data)
self.assertRedirectsNoFollow(res, reverse('nova_instances',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_detail(self):
self.mox.StubOutWithMock(self.project, 'get_images')
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.StubOutWithMock(shortcuts, 'get_user_image_permissions')
self.mox.StubOutWithMock(forms, 'get_key_pair_choices')
self.mox.StubOutWithMock(forms, 'get_instance_type_choices')
self.project.get_images().AndReturn([self.ami])
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
forms.get_key_pair_choices(self.project).AndReturn(
self.create_key_pair_choices([TEST_KEY]))
forms.get_instance_type_choices().AndReturn(
self.create_instance_type_choices())
self.mox.ReplayAll()
res = self.client.get(reverse('nova_images_detail',
args=[TEST_PROJECT, TEST_IMAGE_ID]))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'django_openstack/nova/images/index.html')
self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID)
self.mox.VerifyAll()
def test_remove_form(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.ReplayAll()
res = self.client.get(reverse('nova_images_remove',
args=[TEST_PROJECT, TEST_IMAGE_ID]))
self.assertRedirectsNoFollow(res, reverse('nova_images',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_remove(self):
self.mox.StubOutWithMock(self.project, 'deregister_image')
self.project.deregister_image(TEST_IMAGE_ID).AndReturn(True)
self.mox.ReplayAll()
res = self.client.post(reverse('nova_images_remove',
args=[TEST_PROJECT, TEST_IMAGE_ID]))
self.assertRedirectsNoFollow(res, reverse('nova_images',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_make_public(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.StubOutWithMock(self.project, 'modify_image_attribute')
self.ami.is_public = False
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
self.project.modify_image_attribute(TEST_IMAGE_ID,
attribute='launchPermission',
operation='add').AndReturn(True)
self.mox.ReplayAll()
res = self.client.post(reverse('nova_images_privacy',
args=[TEST_PROJECT, TEST_IMAGE_ID]))
self.assertRedirectsNoFollow(res, reverse('nova_images_detail',
args=[TEST_PROJECT, TEST_IMAGE_ID]))
self.mox.VerifyAll()
def test_make_private(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.StubOutWithMock(self.project, 'modify_image_attribute')
self.ami.is_public = True
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
self.project.modify_image_attribute(TEST_IMAGE_ID,
attribute='launchPermission',
operation='remove').AndReturn(True)
self.mox.ReplayAll()
args = [TEST_PROJECT, TEST_IMAGE_ID]
res = self.client.post(reverse('nova_images_privacy', args=args))
self.assertRedirectsNoFollow(res, reverse('nova_images_detail',
args=args))
self.mox.VerifyAll()
def test_update_form(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
self.mox.ReplayAll()
args = [TEST_PROJECT, TEST_IMAGE_ID]
res = self.client.get(reverse('nova_images_update', args=args))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'django_openstack/nova/images/edit.html')
self.assertEqual(res.context['ami'].id, TEST_IMAGE_ID)
self.mox.VerifyAll()
def test_update(self):
self.mox.StubOutWithMock(self.project, 'get_image')
self.mox.StubOutWithMock(self.project, 'update_image')
self.project.get_image(TEST_IMAGE_ID).AndReturn(self.ami)
self.project.update_image(TEST_IMAGE_ID, 'test', 'test') \
.AndReturn(True)
self.mox.ReplayAll()
args = [TEST_PROJECT, TEST_IMAGE_ID]
data = {'nickname': 'test',
'description': 'test'}
url = reverse('nova_images_update', args=args)
res = self.client.post(url, data)
expected_url = reverse('nova_images_detail', args=args)
self.assertRedirectsNoFollow(res, expected_url)
self.mox.VerifyAll()

View File

@ -1,69 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for instance views.
"""
import boto.ec2.instance
import mox
from django.core.urlresolvers import reverse
from django_openstack.nova.tests.base import (BaseProjectViewTests,
TEST_PROJECT)
TEST_INSTANCE_ID = 'i-abcdefgh'
class InstanceViewTests(BaseProjectViewTests):
def test_index(self):
self.mox.StubOutWithMock(self.project, 'get_instances')
self.project.get_instances().AndReturn([])
self.mox.ReplayAll()
res = self.client.get(reverse('nova_instances', args=[TEST_PROJECT]))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res,
'django_openstack/nova/instances/index.html')
self.assertEqual(len(res.context['instances']), 0)
self.mox.VerifyAll()
def test_detail(self):
instance = boto.ec2.instance.Instance()
instance.id = TEST_INSTANCE_ID
instance.displayName = instance.id
instance.displayDescription = instance.id
self.mox.StubOutWithMock(self.project, 'get_instance')
self.project.get_instance(instance.id).AndReturn(instance)
self.mox.StubOutWithMock(self.project, 'get_instances')
self.project.get_instances().AndReturn([instance])
self.mox.ReplayAll()
res = self.client.get(reverse('nova_instances_detail',
args=[TEST_PROJECT, TEST_INSTANCE_ID]))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res,
'django_openstack/nova/instances/index.html')
self.assertEqual(res.context['selected_instance'].id, instance.id)
self.mox.VerifyAll()

View File

@ -1,93 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for key pair views.
"""
import boto.ec2.keypair
import mox
from django.core.urlresolvers import reverse
from django_openstack.nova.tests.base import (BaseProjectViewTests,
TEST_PROJECT)
TEST_KEY = 'test_key'
class KeyPairViewTests(BaseProjectViewTests):
def test_index(self):
self.mox.StubOutWithMock(self.project, 'get_key_pairs')
self.project.get_key_pairs().AndReturn([])
self.mox.ReplayAll()
response = self.client.get(reverse('nova_keypairs',
args=[TEST_PROJECT]))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response,
'django_openstack/nova/keypairs/index.html')
self.assertEqual(len(response.context['keypairs']), 0)
self.mox.VerifyAll()
def test_add_keypair(self):
key = boto.ec2.keypair.KeyPair()
key.name = TEST_KEY
self.mox.StubOutWithMock(self.project, 'create_key_pair')
self.project.create_key_pair(key.name).AndReturn(key)
self.mox.StubOutWithMock(self.project, 'has_key_pair')
self.project.has_key_pair(key.name).AndReturn(False)
self.mox.ReplayAll()
url = reverse('nova_keypairs_add', args=[TEST_PROJECT])
data = {'js': '0', 'name': key.name}
res = self.client.post(url, data)
self.assertEqual(res.status_code, 200)
self.assertEqual(res['Content-Type'], 'application/binary')
self.mox.VerifyAll()
def test_delete_keypair(self):
self.mox.StubOutWithMock(self.project, 'delete_key_pair')
self.project.delete_key_pair(TEST_KEY).AndReturn(None)
self.mox.ReplayAll()
data = {'key_name': TEST_KEY}
url = reverse('nova_keypairs_delete', args=[TEST_PROJECT])
res = self.client.post(url, data)
self.assertRedirectsNoFollow(res, reverse('nova_keypairs',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_download_keypair(self):
material = 'abcdefgh'
session = self.client.session
session['key.%s' % TEST_KEY] = material
session.save()
res = self.client.get(reverse('nova_keypairs_download',
args=['test', TEST_KEY]))
self.assertEqual(res.status_code, 200)
self.assertEqual(res['Content-Type'], 'application/binary')
self.assertContains(res, material)

View File

@ -1,41 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for region views.
"""
from django.core.urlresolvers import reverse
from django_openstack.nova.tests.base import BaseViewTests
TEST_REGION = 'one'
class RegionViewTests(BaseViewTests):
def test_change(self):
self.authenticateTestUser()
session = self.client.session
session['region'] = 'two'
session.save()
data = {'redirect_url': '/',
'region': TEST_REGION}
res = self.client.post(reverse('region_change'), data)
self.assertEqual(self.client.session['region'], TEST_REGION)
self.assertRedirectsNoFollow(res, '/')

View File

@ -1,187 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import hashlib
import mox
import random
from django import test
from django.conf import settings
from django.db.models.signals import post_save
from django_openstack import models as nova_models
from django_openstack import utils
from django_openstack.core import connection
from nova_adminclient import NovaAdminClient
TEST_USER = 'testUser'
TEST_PROJECT = 'testProject'
TEST_AUTH_TOKEN = hashlib.sha1('').hexdigest()
TEST_AUTH_DATE = utils.utcnow()
TEST_BAD_AUTH_TOKEN = 'badToken'
HOUR = datetime.timedelta(seconds=3600)
AUTH_EXPIRATION_LENGTH = \
datetime.timedelta(days=int(settings.CREDENTIAL_AUTHORIZATION_DAYS))
class CredentialsAuthorizationTests(test.TestCase):
@classmethod
def setUpClass(cls):
# these post_save methods interact with external resources, shut them
# down to test credentials
post_save.disconnect(sender=nova_models.CredentialsAuthorization,
dispatch_uid='django_openstack.CredentialsAuthorization.post_save')
post_save.disconnect(sender=nova_models.CredentialsAuthorization,
dispatch_uid='django_openstack.User.post_save')
def setUp(self):
test_cred = nova_models.CredentialsAuthorization()
test_cred.username = TEST_USER
test_cred.project = TEST_PROJECT
test_cred.auth_date = TEST_AUTH_DATE
test_cred.auth_token = TEST_AUTH_TOKEN
test_cred.save()
badTestCred = nova_models.CredentialsAuthorization()
badTestCred.username = TEST_USER
badTestCred.project = TEST_PROJECT
badTestCred.auth_date = TEST_AUTH_DATE
badTestCred.auth_token = TEST_BAD_AUTH_TOKEN
badTestCred.save()
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def test_get_by_token(self):
TEST_MISSING_AUTH_TOKEN = hashlib.sha1('notAToken').hexdigest()
# Token not a sha1, but exists in system
cred = nova_models.CredentialsAuthorization.get_by_token(
TEST_BAD_AUTH_TOKEN)
self.assertTrue(cred is None)
# Token doesn't exist
cred = nova_models.CredentialsAuthorization.get_by_token(
TEST_MISSING_AUTH_TOKEN)
self.assertTrue(cred is None)
# Good token
cred = nova_models.CredentialsAuthorization.get_by_token(
TEST_AUTH_TOKEN)
self.assertTrue(cred is not None)
# Expire the token
cred.auth_date = utils.utcnow() - AUTH_EXPIRATION_LENGTH \
- HOUR
cred.save()
# Expired token
cred = nova_models.CredentialsAuthorization.get_by_token(
TEST_AUTH_TOKEN)
self.assertTrue(cred is None)
def test_authorize(self):
TEST_USER2 = TEST_USER + '2'
TEST_AUTH_TOKEN_2 = hashlib.sha1('token2').hexdigest()
cred_class = nova_models.CredentialsAuthorization
self.mox.StubOutWithMock(cred_class, 'create_auth_token')
cred_class.create_auth_token(TEST_USER2).AndReturn(
TEST_AUTH_TOKEN_2)
self.mox.ReplayAll()
cred = cred_class.authorize(TEST_USER2, TEST_PROJECT)
self.mox.VerifyAll()
self.assertTrue(cred is not None)
self.assertEqual(cred.username, TEST_USER2)
self.assertEqual(cred.project, TEST_PROJECT)
self.assertEqual(cred.auth_token, TEST_AUTH_TOKEN_2)
self.assertFalse(cred.auth_token_expired())
cred = cred_class.get_by_token(TEST_AUTH_TOKEN_2)
self.assertTrue(cred is not None)
def test_create_auth_token(self):
rand_state = random.getstate()
expected_salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
expected_token = hashlib.sha1(expected_salt + TEST_USER).hexdigest()
random.setstate(rand_state)
auth_token = \
nova_models.CredentialsAuthorization.create_auth_token(TEST_USER)
self.assertEqual(expected_token, auth_token)
def test_auth_token_expired(self):
'''
Test expired in past, expires in future, expires _right now_
'''
cred = \
nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN)
cred.auth_date = utils.utcnow() - AUTH_EXPIRATION_LENGTH \
- HOUR
self.assertTrue(cred.auth_token_expired())
cred.auth_date = utils.utcnow()
self.assertFalse(cred.auth_token_expired())
# testing with time is tricky. Mock out "right now" test to avoid
# timing issues
time = utils.utcnow.override_time = utils.utcnow()
cred.auth_date = time - AUTH_EXPIRATION_LENGTH
self.assertTrue(cred.auth_token_expired())
utils.utcnow.override_time = None
def test_get_download_url(self):
cred = \
nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN)
expected_url = settings.CREDENTIAL_DOWNLOAD_URL + TEST_AUTH_TOKEN
self.assertEqual(expected_url, cred.get_download_url())
def test_get_zip(self):
cred = \
nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN)
admin_mock = self.mox.CreateMock(NovaAdminClient)
self.mox.StubOutWithMock(connection, 'get_nova_admin_connection')
connection.get_nova_admin_connection().AndReturn(admin_mock)
admin_mock.get_zip(TEST_USER, TEST_PROJECT)
self.mox.ReplayAll()
cred.get_zip()
self.mox.VerifyAll()
cred = \
nova_models.CredentialsAuthorization.get_by_token(TEST_AUTH_TOKEN)
self.assertTrue(cred is None)

View File

@ -1,171 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit tests for volume views.
"""
import boto.ec2.volume
import mox
from django.core.urlresolvers import reverse
from django_openstack.nova import forms
from django_openstack.nova.tests.base import (BaseProjectViewTests,
TEST_PROJECT)
TEST_VOLUME = 'vol-0000001'
class VolumeTests(BaseProjectViewTests):
def test_index(self):
instance_id = 'i-abcdefgh'
volume = boto.ec2.volume.Volume()
volume.id = TEST_VOLUME
volume.displayName = TEST_VOLUME
volume.size = 1
self.mox.StubOutWithMock(self.project, 'get_volumes')
self.mox.StubOutWithMock(forms, 'get_available_volume_choices')
self.mox.StubOutWithMock(forms, 'get_instance_choices')
self.project.get_volumes().AndReturn([])
forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn(
self.create_available_volume_choices([volume]))
forms.get_instance_choices(mox.IgnoreArg()).AndReturn(
self.create_instance_choices([instance_id]))
self.mox.ReplayAll()
response = self.client.get(reverse('nova_volumes',
args=[TEST_PROJECT]))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response,
'django_openstack/nova/volumes/index.html')
self.assertEqual(len(response.context['volumes']), 0)
self.mox.VerifyAll()
def test_add_get(self):
self.mox.ReplayAll()
res = self.client.get(reverse('nova_volumes_add', args=[TEST_PROJECT]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_add_post(self):
vol = boto.ec2.volume.Volume()
vol.name = TEST_VOLUME
vol.displayName = TEST_VOLUME
vol.size = 1
self.mox.StubOutWithMock(self.project, 'create_volume')
self.project.create_volume(vol.size, vol.name, vol.name).AndReturn(vol)
self.mox.ReplayAll()
url = reverse('nova_volumes_add', args=[TEST_PROJECT])
data = {'size': '1',
'nickname': TEST_VOLUME,
'description': TEST_VOLUME}
res = self.client.post(url, data)
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_delete_get(self):
self.mox.ReplayAll()
res = self.client.get(reverse('nova_volumes_delete',
args=[TEST_PROJECT, TEST_VOLUME]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_delete_post(self):
self.mox.StubOutWithMock(self.project, 'delete_volume')
self.project.delete_volume(TEST_VOLUME).AndReturn(True)
self.mox.ReplayAll()
res = self.client.post(reverse('nova_volumes_delete',
args=[TEST_PROJECT, TEST_VOLUME]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_attach_get(self):
self.mox.ReplayAll()
res = self.client.get(reverse('nova_volumes_attach',
args=[TEST_PROJECT]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_attach_post(self):
volume = boto.ec2.volume.Volume()
volume.id = TEST_VOLUME
volume.displayName = TEST_VOLUME
volume.size = 1
instance_id = 'i-abcdefgh'
device = '/dev/vdb'
self.mox.StubOutWithMock(self.project, 'attach_volume')
self.mox.StubOutWithMock(forms, 'get_available_volume_choices')
self.mox.StubOutWithMock(forms, 'get_instance_choices')
self.project.attach_volume(TEST_VOLUME, instance_id, device) \
.AndReturn(True)
forms.get_available_volume_choices(mox.IgnoreArg()).AndReturn(
self.create_available_volume_choices([volume]))
forms.get_instance_choices(mox.IgnoreArg()).AndReturn(
self.create_instance_choices([instance_id]))
self.mox.ReplayAll()
url = reverse('nova_volumes_attach', args=[TEST_PROJECT])
data = {'volume': TEST_VOLUME,
'instance': instance_id,
'device': device}
res = self.client.post(url, data)
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_detach_get(self):
self.mox.ReplayAll()
res = self.client.get(reverse('nova_volumes_detach',
args=[TEST_PROJECT, TEST_VOLUME]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()
def test_detach_post(self):
self.mox.StubOutWithMock(self.project, 'detach_volume')
self.project.detach_volume(TEST_VOLUME).AndReturn(True)
self.mox.ReplayAll()
res = self.client.post(reverse('nova_volumes_detach',
args=[TEST_PROJECT, TEST_VOLUME]))
self.assertRedirectsNoFollow(res, reverse('nova_volumes',
args=[TEST_PROJECT]))
self.mox.VerifyAll()

View File

@ -1,23 +0,0 @@
from django_openstack import context_processors, test
class ContextProcessorTests(test.TestCase):
def setUp(self):
super(ContextProcessorTests, self).setUp()
self._prev_catalog = self.request.user.service_catalog
def tearDown(self):
super(ContextProcessorTests, self).tearDown()
self.request.user.service_catalog = self._prev_catalog
def test_object_store(self):
# Returns the object store service data when it's in the catalog
object_store = context_processors.object_store(self.request)
self.assertNotEqual(None, object_store['object_store_configured'])
# Returns None when the object store is not in the catalog
new_catalog = [service for service in self.request.user.service_catalog
if service['type'] != 'object-store']
self.request.user.service_catalog = new_catalog
object_store = context_processors.object_store(self.request)
self.assertEqual(None, object_store['object_store_configured'])

View File

@ -1,71 +0,0 @@
import re
from django import dispatch, http, template
from django.utils.text import normalize_newlines
from django_openstack import signals, test
def single_line(text):
''' Quick utility to make comparing template output easier. '''
return re.sub(' +',
' ',
normalize_newlines(text).replace('\n', '')).strip()
class TemplateTagTests(test.TestCase):
def setUp(self):
super(TemplateTagTests, self).setUp()
self._signal = self.mox.CreateMock(dispatch.Signal)
def test_sidebar_modules(self):
'''
Tests for the sidebar module registration mechanism.
The standard "ping" signal return value looks like this:
tuple(<dash_apps_ping>, {
'title': 'Nixon',
'links': [{'url':'/syspanel/nixon/google',
'text':'Google', 'active_text': 'google'}],
'type': 'syspanel',
})
'''
self.mox.StubOutWithMock(signals, 'dash_modules_detect')
signals_call = (
(self._signal, {
'title': 'Nixon',
'links': [{'url':'/dash/nixon/google',
'text':'Google', 'active_text': 'google'}],
'type': 'dash',
}),
(self._signal, {
'title': 'Nixon',
'links': [{'url':'/syspanel/nixon/google',
'text':'Google', 'active_text': 'google'}],
'type': 'syspanel',
}),
)
signals.dash_modules_detect().AndReturn(signals_call)
signals.dash_modules_detect().AndReturn(signals_call)
self.mox.ReplayAll()
context = template.Context({'request': self.request})
# Dash module is rendered correctly, and only in dash sidebar
ttext = '{% load sidebar_modules %}{% dash_sidebar_modules request %}'
t = template.Template(ttext)
self.assertEqual(single_line(t.render(context)),
'<h3>Nixon</h3> <ul class="sub_nav"> <li>'
'<a href="/dash/nixon/google">Google</a></li> </ul>')
# Syspanel module is rendered correctly and only in syspanel sidebar
ttext = ('{% load sidebar_modules %}'
'{% syspanel_sidebar_modules request %}')
t = template.Template(ttext)
self.assertEqual(single_line(t.render(context)),
'<h3>Nixon</h3> <ul class="sub_nav"> <li>'
'<a href="/syspanel/nixon/google">Google</a></li>'
' </ul>')
self.mox.VerifyAll()

View File

@ -1,77 +0,0 @@
# 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 Nebula 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.
"""
from django import http
from django import shortcuts
from django import test as django_test
from django import template as django_template
from django.conf import settings
from django_openstack import test
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('<html><body><p>'
'This is a fake httpresponse for testing purposes only'
'</p></body></html>')
# Allows django.test.client to populate fields on the response object
django_test.signals.template_rendered.send(template, template=template,
context=context_instance)
return resp
class BaseViewTests(test.TestCase):
def setUp(self):
super(BaseViewTests, self).setUp()
self._real_render_to_response = shortcuts.render_to_response
shortcuts.render_to_response = fake_render_to_response
def tearDown(self):
super(BaseViewTests, self).tearDown()
shortcuts.render_to_response = self._real_render_to_response
def assertRedirectsNoFollow(self, response, expected_url):
self.assertEqual(response._headers['location'],
('Location', settings.TESTSERVER + expected_url))
self.assertEqual(response.status_code, 302)

View File

@ -1,121 +0,0 @@
# 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 Nebula, 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 cloudfiles.errors import ContainerNotEmpty
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 IgnoreArg, IsA
class ContainerViewTests(base.BaseViewTests):
def setUp(self):
super(ContainerViewTests, self).setUp()
self.container = self.mox.CreateMock(api.Container)
self.container.name = 'containerName'
def test_index(self):
self.mox.StubOutWithMock(api, 'swift_get_containers')
api.swift_get_containers(
IsA(http.HttpRequest), marker=None).AndReturn([self.container])
self.mox.ReplayAll()
res = self.client.get(reverse('dash_containers', args=['tenant']))
self.assertTemplateUsed(res,
'django_openstack/dash/containers/index.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(IsA(http.HttpRequest),
'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(
IsA(http.HttpRequest),
'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,
'django_openstack/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(
IsA(http.HttpRequest), 'CreateContainer')
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('dash_containers_create',
args=[self.request.user.tenant_id]),
formData)
self.assertRedirectsNoFollow(res, reverse('dash_containers',
args=[self.request.user.tenant_id]))

View File

@ -1,104 +0,0 @@
# 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 Nebula, 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 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 IgnoreArg, IsA
import quantum.client
class PortViewTests(base.BaseViewTests):
def setUp(self):
super(PortViewTests, self).setUp()
def test_port_create(self):
self.mox.StubOutWithMock(api, "quantum_create_port")
api.quantum_create_port(IsA(http.HttpRequest), 'n1').AndReturn(True)
formData = {'ports_num': 1,
'network': 'n1',
'method': 'CreatePort'}
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('dash_ports_create',
args=[self.request.user.tenant_id, "n1"]),
formData)
self.assertRedirectsNoFollow(res, reverse('dash_networks_detail',
args=[self.request.user.tenant_id,
"n1"]))
def test_port_delete(self):
self.mox.StubOutWithMock(api, "quantum_delete_port")
api.quantum_delete_port(IsA(http.HttpRequest),
'n1', 'p1').AndReturn(True)
formData = {'port': 'p1',
'network': 'n1',
'method': 'DeletePort'}
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('dash_networks_detail',
args=[self.request.user.tenant_id, "n1"]),
formData)
def test_port_attach(self):
self.mox.StubOutWithMock(api, "quantum_attach_port")
api.quantum_attach_port(IsA(http.HttpRequest),
'n1', 'p1', dict).AndReturn(True)
formData = {'port': 'p1',
'network': 'n1',
'vif_id': 'v1',
'method': 'AttachPort'}
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('dash_ports_attach',
args=[self.request.user.tenant_id, "n1", "p1"]),
formData)
self.assertRedirectsNoFollow(res, reverse('dash_networks_detail',
args=[self.request.user.tenant_id,
"n1"]))
def test_port_detach(self):
self.mox.StubOutWithMock(api, "quantum_detach_port")
api.quantum_detach_port(IsA(http.HttpRequest),
'n1', 'p1').AndReturn(True)
formData = {'port': 'p1',
'network': 'n1',
'method': 'DetachPort'}
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('dash_networks_detail',
args=[self.request.user.tenant_id, "n1"]),
formData)

View File

@ -1,68 +0,0 @@
#!/usr/bin/env python
"""Generates files for sphinx documentation using a simple Autodoc based
template.
To use, just run as a script:
$ python doc/generate_autodoc_index.py
"""
import os
base_dir = os.path.dirname(os.path.abspath(__file__))
RSTDIR = os.path.join(base_dir, "source", "sourcecode")
SRCS = {'dashboard': os.path.join(base_dir, "..", "openstack-dashboard"),
'django_openstack': os.path.join(base_dir, "..", "django-openstack")}
def find_autodoc_modules(module_name, sourcedir):
"""returns a list of modules in the SOURCE directory"""
modlist = []
os.chdir(os.path.join(sourcedir, module_name))
print "SEARCHING %s" % sourcedir
for root, dirs, files in os.walk("."):
for filename in files:
if filename.endswith(".py"):
# root = ./dashboard/test/unit
# filename = base.py
# remove the pieces of the root
elements = root.split(os.path.sep)
# replace the leading "." with the module name
elements[0] = module_name
# and get the base module name
base, extension = os.path.splitext(filename)
if not (base == "__init__"):
elements.append(base)
result = ".".join(elements)
#print result
modlist.append(result)
return modlist
if not(os.path.exists(RSTDIR)):
os.mkdir(RSTDIR)
INDEXOUT = open("%s/autoindex.rst" % RSTDIR, "w")
INDEXOUT.write("Source Code Index\n")
INDEXOUT.write("=================\n")
INDEXOUT.write(".. toctree::\n")
INDEXOUT.write(" :maxdepth: 1\n")
INDEXOUT.write("\n")
for modulename in SRCS:
for module in find_autodoc_modules(modulename, SRCS[modulename]):
generated_file = "%s/%s.rst" % (RSTDIR, module)
print "Generating %s" % generated_file
INDEXOUT.write(" %s\n" % module)
FILEOUT = open(generated_file, "w")
FILEOUT.write("The :mod:`%s` Module\n" % module)
FILEOUT.write("=============================="
"=============================="
"==============================\n")
FILEOUT.write(".. automodule:: %s\n" % module)
FILEOUT.write(" :members:\n")
FILEOUT.write(" :undoc-members:\n")
FILEOUT.write(" :show-inheritance:\n")
FILEOUT.close()
INDEXOUT.close()

View File

@ -1,69 +0,0 @@
..
Copyright 2011 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
========================
Horizon for Contributors
========================
Horizon is the canonical implementation of `Openstack's Dashboard
<https://github.com/openstack/horizon>`_, which provides a web based user
interface to OpenStack services including Nova, Swift, Keystone, and Quantum.
This document describes horizon for contributors of the project.
Project Structure
=================
This project is a bit different from other Openstack projects in that it has
two very distinct components underneath it:
* django-openstack
* openstack-dashboard
Django-openstack holds the generic libraries and components that can be
used in any Django project. In testing, this component is set up with
buildout (see run_tests.sh), and any dependencies that get added need to
be added to the django-openstack/buildout.cfg file.
Openstack-dashboard is a reference django project that uses django-openstack
and is built with a virtualenv and tested through that environment. If
depdendencies are added that the reference django project needs, they
should be added to openstack-dashboard/tools/pip-requires.
Contents:
---------
.. toctree::
:maxdepth: 1
testing
Developer Docs
--------------
.. toctree::
:maxdepth: 1
sourcecode/autoindex
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,32 +0,0 @@
..
Copyright 2011 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
=====================
Testing the Dashboard
=====================
Testing the dashbaord is a bit more complex due to having the two projects
in the same repository.
The run_tests.sh script invokes tests and analysis on both of these
components in it's process, and is what Jenkins uses to verify the
stability of the project.
To run the tests::
$ ./run_tests.sh

View File

@ -12,7 +12,93 @@
# serve to show the default.
import sys, os
from django_openstack import version as horizon_version
import horizon.version
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
HORIZON_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "horizon"))
DASHBOARD_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "openstack-dashboard"))
sys.path.insert(0, HORIZON_DIR)
sys.path.insert(0, DASHBOARD_DIR)
def write_autodoc_index():
def find_autodoc_modules(module_name, sourcedir):
"""returns a list of modules in the SOURCE directory"""
modlist = []
os.chdir(os.path.join(sourcedir, module_name))
print "SEARCHING %s" % sourcedir
for root, dirs, files in os.walk("."):
for filename in files:
if filename.endswith(".py"):
# root = ./dashboard/test/unit
# filename = base.py
# remove the pieces of the root
elements = root.split(os.path.sep)
# replace the leading "." with the module name
elements[0] = module_name
# and get the base module name
base, extension = os.path.splitext(filename)
if not (base == "__init__"):
elements.append(base)
result = ".".join(elements)
#print result
modlist.append(result)
return modlist
RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode"))
SRCS = {'horizon': HORIZON_DIR,
'dashboard': DASHBOARD_DIR}
if not(os.path.exists(RSTDIR)):
os.mkdir(RSTDIR)
INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w")
INDEXOUT.write("=================\n")
INDEXOUT.write("Source Code Index\n")
INDEXOUT.write("=================\n")
for modulename, path in SRCS.items():
sys.stdout.write("Generating source documentation for %s\n" % modulename)
INDEXOUT.write("\n%s\n" % modulename.capitalize())
INDEXOUT.write("%s\n" % ("=" * len(modulename),))
INDEXOUT.write(".. toctree::\n")
INDEXOUT.write(" :maxdepth: 1\n")
INDEXOUT.write("\n")
if not(os.path.exists(os.path.join(RSTDIR, modulename))):
os.mkdir(os.path.join(RSTDIR, modulename))
for module in find_autodoc_modules(modulename, path):
mod_path = os.path.join(path, *module.split("."))
generated_file = os.path.join(RSTDIR, modulename, "%s.rst" % module)
INDEXOUT.write(" %s/%s\n" % (modulename, module))
# Find the __init__.py module if this is a directory
if os.path.isdir(mod_path):
source_file = ".".join((os.path.join(mod_path, "__init__"), "py",))
else:
source_file = ".".join((os.path.join(mod_path), "py"))
# Only generate a new file if the source has changed or we don't
# have a doc file to begin with.
if not os.access(generated_file, os.F_OK) or \
os.stat(generated_file).st_mtime < os.stat(source_file).st_mtime:
print "Module %s updated, generating new documentation." % module
FILEOUT = open(generated_file, "w")
header = "The :mod:`%s` Module" % module
FILEOUT.write("%s\n" % ("=" * len(header),))
FILEOUT.write("%s\n" % header)
FILEOUT.write("%s\n" % ("=" * len(header),))
FILEOUT.write(".. automodule:: %s\n" % module)
FILEOUT.write(" :members:\n")
FILEOUT.write(" :undoc-members:\n")
FILEOUT.write(" :show-inheritance:\n")
FILEOUT.write(" :noindex:\n")
FILEOUT.close()
INDEXOUT.close()
write_autodoc_index()
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -57,9 +143,9 @@ copyright = u'2011, OpenStack, LLC'
# built documents.
#
# The short X.Y version.
version = horizon_version.canonical_version_string()
version = horizon.version.canonical_version_string()
# The full version, including alpha/beta/rc tags.
release = horizon_version.canonical_version_string()
release = horizon.version.canonical_version_string()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -95,6 +181,9 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
primary_domain = 'py'
nitpicky = False
# -- Options for HTML output ---------------------------------------------------
@ -295,6 +384,7 @@ epub_copyright = u'2011, OpenStack'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'python': ('http://docs.python.org/', None),
'django': ('http://docs.djangoproject.com/en/dev/_objects/'),
'nova': ('http://nova.openstack.org', None),
'swift': ('http://swift.openstack.org', None),
'keystone': ('http://keystone.openstack.org', None),

37
docs/source/faq.rst Normal file
View File

@ -0,0 +1,37 @@
==========================
Frequently Asked Questions
==========================
What is the relationship between ``Dashboards``, ``Panels``, and navigation?
The navigational structure is strongly encouraged to flow from
``Dashboard`` objects as top-level navigation items to ``Panel`` objects as
sub-navigation items as in the current implementation. Template tags
are provided to automatically generate this structure.
That said, you are not required to use the provided tools and can write
templates and URLconfs by hand to create any desired structure.
Does a panel have to be an app in ``INSTALLED_APPS``?
A panel can live in any Python module. It can be a standalone which ties
into an existing dashboard, or it can be contained alongside others within
a larger dashboard "app". There is no strict enforcement here. Python
is "a language for consenting adults." A module containing a Panel does
not need to be added to ``INSTALLED_APPS``, but this is a common and
convenient way to load a standalone panel.
Could I hook an external service into a panel using, for example, an iFrame?
Panels are just entry-points to hook views into the larger dashboard
navigational structure and enforce common attributes like RBAC. The
view and corresponding templates can contain anything you would like,
including iFrames.
What does this mean for visual design?
The ability to add an arbitrary number of top-level navigational items
(``Dashboard`` objects) poses a new design challenge. Horizon's lead
designer has taken on the challenge of providing a reference design
for Horizon which supports this possibility.

19
docs/source/glossary.rst Normal file
View File

@ -0,0 +1,19 @@
========
Glossary
========
Horizon
The OpenStack dashboard project. Also the name of the top-level
Python object which handles registration for the app.
Dashboard
A Python class representing a top-level navigation item (e.g. "syspanel")
which provides a consistent API for Horizon-compatible applications.
Panel
A Python class representing a sub-navigation item (e.g. "instances")
which contains all the necessary logic (views, forms, tests, etc.) for
that interface.

101
docs/source/index.rst Normal file
View File

@ -0,0 +1,101 @@
..
Copyright 2011 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
========================================
Horizon: The OpenStack Dashboard Project
========================================
Introduction
============
Horizon is the canonical implementation of `Openstack's Dashboard
<https://github.com/openstack/horizon>`_, which provides a web based user
interface to OpenStack services including Nova, Swift, Keystone, etc.
For a more in-depth look at Horizon and it's architecture, see the
:doc:`Introduction to Horizon <intro>`.
To learn what you need to know to get going, see the :doc:`quickstart`.
Getting Started With Horizon
============================
How to use Horizon in your own projects.
.. toctree::
:maxdepth: 1
intro
quickstart
Developer Reference
===================
For those wishing to develop Horizon itself, or go in-depth with building
your own :class:`~horizon.Dashboard` or :class:`~horizon.Panel` classes,
the following documentation is provided.
Topics
------
Brief guides to areas of interest and importance when developing Horizon.
.. toctree::
:maxdepth: 1
testing
API Reference
-------------
In-depth documentation for Horizon and it's APIs.
.. toctree::
:maxdepth: 1
ref/run_tests
ref/horizon
ref/users
ref/forms
ref/views
ref/middleware
ref/context_processors
ref/decorators
ref/exceptions
Source Code Reference
---------------------
Auto-generated reference for the complete source code.
.. toctree::
:maxdepth: 1
sourcecode/autoindex
Information
===========
.. toctree::
:maxdepth: 1
faq
glossary
* :ref:`genindex`
* :ref:`modindex`

124
docs/source/intro.rst Normal file
View File

@ -0,0 +1,124 @@
===================
Introducing Horizon
===================
.. contents:: Contents:
:local:
Values
======
"Think simple" as my old master used to say - meaning reduce
the whole of its parts into the simplest terms, getting back
to first principles.
-- Frank Lloyd Wright
Horizon holds several key values at the core of it's design and architecture:
* Core Support: Out-of-the-box support for all core OpenStack projects.
* Extensible: Anyone can add a new component as a "first-class citizen".
* Manageable: The core codebase should be simple and easy-to-navigate.
* Consistent: Visual and interaction paradigms are maintained throughout.
* Stable: A reliable API with an emphasis on backwards-compatibility.
* Usable: Providing an *awesome* interface that people *want* to use.
The only way to attain and uphold those ideals is to make it *easy* for
developers to implement those values.
History
=======
Horizon started life as a single app to manage OpenStack's compute project.
As such, all it needed was a set of views, templates, and API calls.
From there it grew to support multiple OpenStack projects and APIs gradually,
arranged rigidly into "dash" and "syspanel" groupings.
During the "Diablo" release cycle an initial plugin system was added using
signals to hook in additional URL patterns and add links into the "dash"
and "syspanel" navigation.
This incremental growth served the goal of "Core Support" phenomenally, but
left "Extensible" and "Manageable" behind. And while the other key values took
shape of their own accord, it was time to re-architect for an extensible,
modular future.
The Current Architecture & How It Meets Our Values
==================================================
At it's core, **Horizon should be a registration pattern for
applications to hook into**. Here's what that means and how it's
implemented in terms of our values:
Core Support
------------
Horizon ships with three central dashboards, a "User Dashboard", a
"System Dashboard", and a "Settings" dashboard. Between these three they
cover the core OpenStack applications and deliver on Core Support.
The Horizon application also ships with a set of API abstractions
for the core OpenStack projects in order to provide a consistent, stable set
of reusable methods for developers. Using these abstractions, developers
working on Horizon don't need to be intimately familiar with the APIs of
each OpenStack project.
Extensible
----------
A Horizon dashboard application is based around the :class:`~horizon.Dashboard`
class that provides a consistent API and set of capabilities for both
core OpenStack dashboard apps shipped with Horizon and equally for third-party
apps. The :class:`~horizon.Dashboard` class is treated as a top-level
navigation item.
Should a developer wish to provide functionality within an existing dashboard
(e.g. adding a monitoring panel to the user dashboard) the simple registration
pattern makes it possible to write an app which hooks into other dashboards
just as easily as creating a new dashboard. All you have to do is import the
dashboard you wish to modify.
Manageable
----------
Within the application, there is a simple method for registering a
:class:`~horizon.Panel` (sub-navigation items). Each panel contains the
necessary logic (views, forms, tests, etc.) for that interface. This granular
breakdown prevents files (such as ``api.py``) from becoming thousands of
lines long and makes code easy to find by correlating it directly to the
navigation.
Consistent
----------
By providing the necessary core classes to build from, as well as a
solid set of reusable templates and additional tools (base form classes,
base widget classes, template tags, and perhaps even class-based views)
we can maintain consistency across applications.
Stable
------
By architecting around these core classes and reusable components we
create an implicit contract that changes to these components will be
made in the most backwards-compatible ways whenever possible.
Usable
------
Ultimately that's up to each and every developer that touches the code,
but if we get all the other goals out of the way then we are free to focus
on the best possible experience.
.. seealso::
:doc:`Quickstart <quickstart>`
A short guide to getting started with using Horizon.
:doc:`Frequently Asked Questions <faq>`
Common questions and answers.
:doc:`Glossary <glossary>`
Common terms and their definitions.

146
docs/source/quickstart.rst Normal file
View File

@ -0,0 +1,146 @@
==================
Horizon Quickstart
==================
Horizon's Structure
===================
This project is a bit different from other Openstack projects in that it is
composed of two distinct components:
* ``horizon``
* ``openstack-dashboard``
The ``horizon`` directory holds the generic libraries and components that can
be used in any Django project. In testing, this component is set up with
buildout (see :doc:`ref/run_tests`), and any dependencies that need to
be added to the ``horizon/buildout.cfg`` file.
The ``openstack-dashboard`` directory contains a reference Django project that
uses ``horizon`` and is built with a virtualenv. If dependencies are added that
``openstack-dashboard`` requires they should be added to ``openstack-
dashboard/tools/pip-requires``.
Project
=======
INSTALLED_APPS
--------------
At the project level you add Horizon and any desired dashboards to your
``settings.INSTALLED_APPS``::
INSTALLED_APPS = (
'django',
...
'horizon',
'horizon.dash',
'horizon.syspanel',
)
URLs
----
Then you add a single line to your project's ``urls.py``::
url(r'', include(horizon.urls)),
Those urls are automatically constructed based on the registered Horizon apps.
If a different URL structure is desired it can be constructed by hand.
Templates
---------
Pre-built template tags generate navigation. In your ``nav.html``
template you might have the following::
{% load horizon %}
<div class='nav'>
{% horizon_main_nav %}
</div>
And in your ``sidebar.html`` you might have::
{% load horizon %}
<div class='sidebar'>
{% horizon_dashboard_nav %}
</div>
These template tags are aware of the current "active" dashboard and panel
via template context variables and will render accordingly.
Application
===========
Structure
---------
An application would have the following structure (we'll use syspanel as
an example)::
syspanel/
|---__init__.py
|---dashboard.py <-----Registers the app with Horizon and sets dashboard properties
|---templates/
|---templatetags/
|---overview/
|---services/
|---images/
|---__init__.py
|---panel.py <-----Registers the panel in the app and defines panel properties
|---urls.py
|---views.py
|---forms.py
|---tests.py
|---api.py <-------Optional additional API methods for non-core services
|---templates/
...
...
Dashboard Classes
-----------------
Inside of ``dashboard.py`` you would have a class definition and the registration
process::
import horizon
class Syspanel(horizon.Dashboard):
name = "Syspanel" # Appears in navigation
slug = 'syspanel' # Appears in url
panels = ('overview', 'services', 'instances', 'flavors', 'images',
'tenants', 'users', 'quotas',)
default_panel = 'overview'
roles = ('admin',) # Provides RBAC at the dashboard-level
...
horizon.register(Syspanel)
Panel Classes
-------------
To connect a :class:`~horizon.Panel` with a :class:`~horizon.Dashboard` class
you register it in a ``panels.py`` file like so::
import horizon
from horizon.dashboard.syspanel import dashboard
class Images(horizon.Panel):
name = "Images"
slug = 'images'
roles = ('admin', 'my_other_role',) # Fine-grained RBAC per-panel
# You could also register your panel with another application's dashboard
dashboard.Syspanel.register(Images)
By default a :class:`~horizon.Panel` class looks for a ``urls.py`` file in the
same directory as ``panel.py`` to include in the rollup of url patterns from
panels to dashboards to Horizon, resulting in a wholly extensible, configurable
URL structure.

View File

@ -0,0 +1,6 @@
==========================
Horizon Context Processors
==========================
.. automodule:: horizon.context_processors
:members:

View File

@ -0,0 +1,6 @@
==================
Horizon Decorators
==================
.. automodule:: horizon.decorators
:members:

View File

@ -0,0 +1,6 @@
==================
Horizon Exceptions
==================
.. automodule:: horizon.exceptions
:members:

17
docs/source/ref/forms.rst Normal file
View File

@ -0,0 +1,17 @@
=============
Horizon Forms
=============
Horizon ships with a number of form classes, some generic and some specific.
Generic Forms
=============
.. automodule:: horizon.forms
:members:
Auth Forms
==========
.. automodule:: horizon.views.auth_forms
:members:

View File

@ -0,0 +1,42 @@
======================
The ``horizon`` Module
======================
.. module:: horizon
Horizon ships with a single point of contact for hooking into your project if
you aren't developing your own :class:`~horizon.Dashboard` or
:class:`~horizon.Panel`::
import horizon
From there you can access all the key methods you need.
Horizon
=======
.. attribute:: urls
The auto-generated URLconf for Horizon. Usage::
url(r'', include(horizon.urls)),
.. autofunction:: register
.. autofunction:: unregister
.. autofunction:: get_absolute_url
.. autofunction:: get_user_home
.. autofunction:: get_dashboard
.. autofunction:: get_default_dashboard
.. autofunction:: get_dashboards
Dashboard
=========
.. autoclass:: Dashboard
:members:
Panel
=====
.. autoclass:: Panel
:members:

View File

@ -0,0 +1,6 @@
==================
Horizon Middleware
==================
.. automodule:: horizon.middleware
:members:

View File

@ -0,0 +1,100 @@
===========================
The ``run_tests.sh`` Script
===========================
Horizon ships with a script called ``run_tests.sh`` at the root of the
repository. This script provides many crucial functions for the project,
and also makes several otherwise complex tasks trivial for you as a
developer.
First Run
=========
If you start with a clean copy of the Horizon repository, the first thing
you should do is to run ``./run_tests.sh`` from the root of the repository.
This will do three things for you:
#. Set up a virtual environment for ``openstack-dashboard`` using
``openstack-dashboard/tools/install_venv.py``.
#. Set up an environment for ``horizon`` using
``horizon/bootstrap.py`` and ``horizon/bin/buildout``.
#. Run the tests for both ``horizon`` and ``openstack-dashboard`` using
their respective environments and verify that evreything is working.
Setting up both environments the first time can take several minutes, but only
needs to be done once. If dependencies are added in the future, updating the
environments will be necessary but not necessarily as time consuming.
I just want to run the tests!
=============================
Running both sets of unit tests quickly and easily is the main goal of this
script. All you need to do is::
./run_tests.sh
Yep, that's it. Everything else the script can do is optional.
Give me metrics!
================
You can generate various reports and metrics using command line arguments
to ``run_tests.sh``.
Coverage
--------
To run coverage reports::
./run_tests.sh --coverage
The reports are saved to ``./reports/`` and ``./coverage.xml``.
PEP8
----
You can check for PEP8 violations as well::
./run_tests.sh --pep8
The results are saved to ``./pep8.txt``.
PyLint
------
For more detailed code analysis you can run::
./run_tests.sh --pylint
The output will be saved in ``./pylint.txt``.
Running the development server
==============================
As an added bonus, you can run Django's development server directly from
the root of the repository with ``run_tests.sh`` like so::
./run_tests.sh --runserver
This is effectively just an alias for::
./openstack-dashboard/tools/with_venv.sh ./openstack-dashboard/dashboard/manage.py runserver
Generating the documentation
============================
You can build Horizon's documentation automatically by running::
./run_tests.sh --docs
The output is stored in ``./docs/build/html/``.
Starting clean
==============
If you ever want to start clean with a new environment for Horizon, you can
run::
./run_tests.sh --force
That will blow away the existing environments and create new ones for you.

View File

@ -0,0 +1,6 @@
=================
Horizon User APIs
=================
.. automodule:: horizon.users
:members:

12
docs/source/ref/views.rst Normal file
View File

@ -0,0 +1,12 @@
=============
Horizon Views
=============
Horizon ships with a number of pre-built views which are used within
Horizon and can also be reused in your applications.
Auth
====
.. automodule:: horizon.views.auth
:members:

62
docs/source/testing.rst Normal file
View File

@ -0,0 +1,62 @@
..
Copyright 2011 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
===============
Testing Horizon
===============
How to run the tests
====================
Because Horizon is composed of both the ``horizon`` app and the
``openstack-dashboard`` reference project, there are in fact two sets of unit
tests. While they can be run individually without problem, there is an easier
way:
Included at the root of the repository is the ``run_tests.sh`` script
which invokes both sets of tests, and optionally generates analyses on both
components in the process. This script is what what Jenkins uses to verify the
stability of the project, so you should make sure you run it and it passes
before you submit any pull requests/patches.
To run the tests::
$ ./run_tests.sh
.. seealso::
:doc:`ref/run_tests`
Full reference for the ``run_tests.sh`` script.
How to write good tests
=======================
Horizon uses Django's unit test machinery (which extends Python's ``unittest2``
library) as the core of it's test suite. As such, all tests for the Python code
should be written as unit tests. No doctests please.
A few pointers for writing good tests:
* Write tests as you go--If you save them to the end you'll write less of
them and they'll often miss large chunks of code.
* Keep it as simple as possible--Make sure each test tests one thing
and tests it thoroughly.
* Think about all the possible inputs your code could have--It's usually
the edge cases that end up revealing bugs.
* Use ``coverage.py`` to find out what code is *not* being tested.
In general new code without unit tests will not be accepted, and every bugfix
*must* include a regression test.

View File

@ -1,7 +1,7 @@
PYTHON=`which python`
DESTDIR=/
BUILDIR=$(CURDIR)/debian/django-openstack
PROJECT=django-openstack
BUILDIR=$(CURDIR)/debian/horizon
PROJECT=horizon
all:
@echo "make buildout - Run through buildout"

59
horizon/README Normal file
View File

@ -0,0 +1,59 @@
========================================
Horizon: The OpenStack Dashboard Project
========================================
The Horizon project is a Django module that is used to provide web based
interactions with an OpenStack cloud.
There is a reference implementation that uses this module located at:
http://launchpad.net/horizon
It is highly recommended that you make use of this reference implementation
so that changes you make can be visualized effectively and are consistent.
Using this reference implementation as a development environment will greatly
simplify development of the ``horizon`` module.
Of course, if you are developing your own Django site using Horizon, then
you can disregard this advice.
Getting Started
===============
Horizon uses Buildout (http://www.buildout.org/) to manage local 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
This will install all the dependencies of Horizon and provide some useful
scripts in the ``bin/`` directory:
bin/python provides a python shell for the current buildout.
bin/django provides django functions for the current buildout.
You should now be able to run unit tests as follows::
$ bin/django test
or::
$ bin/test
You can run unit tests with code coverage on Horizon by setting
``NOSE_WITH_COVERAGE``::
$ NOSE_WITH_COVERAGE=true bin/test
Get even better coverage info by running coverage directly::
$ coverage run --branch --source horizon bin/django test horizon && coverage html

View File

@ -26,6 +26,7 @@ webob = 1.0.8
# or can be fetched from pypi
recipe = zc.recipe.egg
eggs =
python-dateutil
django-mailer
httplib2
python-cloudfiles
@ -56,9 +57,9 @@ eggs =
interpreter = python
[django-openstack]
[horizon]
recipe = zc.recipe.egg
eggs = django-openstack
eggs = horizon
interpreter = python
@ -69,13 +70,13 @@ interpreter = python
# 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
project = horizon
projectegg = horizon
settings = tests
test = django_openstack
test = horizon
eggs =
${dependencies:eggs}
${django-openstack:eggs}
${horizon:eggs}
${glance-dependencies:eggs}
extra-paths =
${buildout:directory}/parts/openstack-compute

View File

@ -0,0 +1,50 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, 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.
""" The Horizon OpenStack Dashboard interface.
Contains the core Horizon classes--:class:`~horizon.Dashboard` and
:class:`horizon.Panel`--the dynamic URLconf for Horizon, and common interface
methods like :func:`~horizon.register` and :func:`~horizon.unregister`.
"""
# Because this module is compiled by setup.py before Django may be installed
# in the environment we try importing Django and issue a warning but move on
# should that fail.
django = None
try:
import django
except ImportError:
import warnings
def simple_warn(message, category, filename, lineno, file=None, line=None):
return '%s: %s' % (category.__name__, message)
msg = ("Could not import Django. This is normal during installation.\n")
warnings.formatwarning = simple_warn
warnings.warn(msg, Warning)
if django:
from horizon.base import Horizon, Dashboard, Panel, Workflow
register = Horizon.register
unregister = Horizon.unregister
get_absolute_url = Horizon.get_absolute_url
get_user_home = Horizon.get_user_home
get_dashboard = Horizon.get_dashboard
get_default_dashboard = Horizon.get_default_dashboard
get_dashboards = Horizon.get_dashboards
urls = Horizon._lazy_urls

View File

@ -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 Nebula, 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, Horizon developers not working on horizon.api
shouldn't need to understand the finer details of APIs for
Keystone/Nova/Glance/Swift et. al.
"""
from horizon.api.glance import *
from horizon.api.keystone import *
from horizon.api.nova import *
from horizon.api.swift import *
from horizon.api.quantum import *

118
horizon/horizon/api/base.py Normal file
View File

@ -0,0 +1,118 @@
# 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 Nebula, 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 functools
import logging
from django.utils.decorators import available_attrs
from horizon import exceptions
__all__ = ('APIResourceWrapper', 'APIDictWrapper',
'get_service_from_catalog', 'url_for',)
LOG = logging.getLogger(__name__)
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
def get_service_from_catalog(catalog, service_type):
for service in catalog:
if service['type'] == service_type:
return service
return None
def url_for(request, service_type, admin=False):
catalog = request.user.service_catalog
service = get_service_from_catalog(catalog, service_type)
if service:
try:
if admin:
return service['endpoints'][0]['adminURL']
else:
return service['endpoints'][0]['internalURL']
except (IndexError, KeyError):
raise exceptions.ServiceCatalogException(service_type)
else:
raise exceptions.ServiceCatalogException(service_type)

View File

@ -0,0 +1,95 @@
# 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 Nebula, 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 functools
import logging
from django.utils.decorators import available_attrs
import openstack.compute
import openstackx.admin
import openstackx.api.exceptions as openstackx_exceptions
import openstackx.extras
from horizon.api.base import *
LOG = logging.getLogger(__name__)
REBOOT_HARD = openstack.compute.servers.REBOOT_HARD
def check_openstackx(f):
"""Decorator that adds extra info to api exceptions
The OpenStack 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
Horizon more gracefully handle extensions and openstackx extensions
aren't required by Horizon in Nova.
"""
@functools.wraps(f, assigned=available_attrs(f))
def inner(*args, **kwargs):
try:
return f(*args, **kwargs)
except openstackx_exceptions.NotFound, e:
e.message = e.details or ''
e.message += ' This error may be caused by a misconfigured' \
' Nova url in keystone\'s service catalog, or ' \
' by missing openstackx extensions in Nova. ' \
' See the Horizon README.'
raise
return inner
def compute_api(request):
management_url = url_for(request, 'compute')
compute = openstack.compute.Compute(
auth_token=request.user.token,
management_url=management_url)
# 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.user.token
compute.client.management_url = management_url
LOG.debug('compute_api connection created using token "%s"'
' and url "%s"' %
(request.user.token, management_url))
return compute
def admin_api(request):
management_url = url_for(request, 'compute', True)
LOG.debug('admin_api connection created using token "%s"'
' and url "%s"' %
(request.user.token, management_url))
return openstackx.admin.Admin(auth_token=request.user.token,
management_url=management_url)
def extras_api(request):
management_url = url_for(request, 'compute')
LOG.debug('extras_api connection created using token "%s"'
' and url "%s"' %
(request.user.token, management_url))
return openstackx.extras.Extras(auth_token=request.user.token,
management_url=management_url)

View File

@ -0,0 +1,89 @@
# 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 Nebula, 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 __future__ import absolute_import
import logging
import urlparse
from glance import client as glance_client
from horizon.api.base import *
LOG = logging.getLogger(__name__)
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', 'owner']
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']
def glance_api(request):
o = urlparse.urlparse(url_for(request, 'image'))
LOG.debug('glance_api connection created for host "%s:%d"' %
(o.hostname, o.port))
return glance_client.Client(o.hostname,
o.port,
auth_tok=request.user.token)
def image_create(request, image_meta, image_file):
return Image(glance_api(request).add_image(image_meta, image_file))
def image_delete(request, image_id):
return glance_api(request).delete_image(image_id)
def image_get(request, image_id):
return Image(glance_api(request).get_image(image_id)[0])
def image_list_detailed(request):
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 Image(glance_api(request).update_image(image_id,
image_meta=image_meta))
def snapshot_list_detailed(request):
filters = {}
filters['property-image_type'] = 'snapshot'
filters['is_public'] = 'none'
return [Image(i) for i in glance_api(request)
.get_images_detailed(filters=filters)]

View File

@ -0,0 +1,256 @@
# 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 Nebula, 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.conf import settings
from keystoneclient import exceptions as keystone_exceptions
from keystoneclient.v2_0 import client as keystone_client
from horizon.api.base import *
from horizon.api.deprecated import admin_api
from horizon.api.deprecated import check_openstackx
LOG = logging.getLogger(__name__)
class Tenant(APIResourceWrapper):
"""Simple wrapper around keystoneclient.tenants.Tenant"""
_attrs = ['id', 'description', 'enabled', 'name']
class Token(APIResourceWrapper):
"""Simple wrapper around keystoneclient.tokens.Tenant"""
_attrs = ['id', 'user', 'serviceCatalog', 'tenant']
class User(APIResourceWrapper):
"""Simple wrapper around keystoneclient.users.User"""
_attrs = ['email', 'enabled', 'id', 'tenantId', 'name']
class Role(APIResourceWrapper):
"""Wrapper around keystoneclient.roles.role"""
_attrs = ['id', 'name', 'description', 'service_id']
class Services(APIResourceWrapper):
_attrs = ['disabled', 'host', 'id', 'last_update', 'stats', 'type', 'up',
'zone']
def keystoneclient(request, username=None, password=None, tenant_id=None,
token_id=None, endpoint=None):
"""Returns a client connected to the Keystone backend.
Several forms of authentication are supported:
* Username + password -> Unscoped authentication
* Username + password + tenant id -> Scoped authentication
* Unscoped token -> Unscoped authentication
* Unscoped token + tenant id -> Scoped authentication
* Scoped token -> Scoped authentication
Available services and data from the backend will vary depending on
whether the authentication was scoped or unscoped.
Lazy authentication if an ``endpoint`` parameter is provided.
The client is cached so that subsequent API calls during the same
request/response cycle don't have to be re-authenticated.
"""
# Take care of client connection caching/fetching a new client
user = request.user
if hasattr(request, '_keystone') and \
request._keystone.auth_token == user.token:
conn = request._keystone
else:
conn = keystone_client.Client(username=username or user.username,
password=password,
project_id=tenant_id or user.tenant_id,
token=token_id or user.token,
auth_url=settings.OPENSTACK_KEYSTONE_URL,
endpoint=endpoint)
request._keystone = conn
# Fetch the correct endpoint for the user type
catalog = getattr(conn, 'service_catalog', None)
if catalog and "serviceCatalog" in catalog.catalog.keys():
if user.is_admin():
endpoint = catalog.url_for(service_type='identity',
endpoint_type='adminURL')
else:
endpoint = catalog.url_for(service_type='identity',
endpoint_type='publicURL')
else:
endpoint = settings.OPENSTACK_KEYSTONE_URL
conn.management_url = endpoint
return conn
def tenant_create(request, tenant_name, description, enabled):
return Tenant(keystoneclient(request).tenants.create(tenant_name,
description,
enabled))
def tenant_get(request, tenant_id):
return Tenant(keystoneclient(request).tenants.get(tenant_id))
def tenant_delete(request, tenant_id):
keystoneclient(request).tenants.delete(tenant_id)
def tenant_list(request):
return [Tenant(t) for t in keystoneclient(request).tenants.list()]
def tenant_update(request, tenant_id, tenant_name, description, enabled):
return Tenant(keystoneclient(request).tenants.update(tenant_id,
tenant_name,
description,
enabled))
def tenant_delete(request, tenant_id):
keystoneclient(request).tenants.delete(tenant_id)
def tenant_list_for_token(request, token):
c = keystoneclient(request, token_id=token,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
return [Tenant(t) for t in c.tenants.list()]
def token_create(request, tenant, username, password):
'''
Creates a token using the username and password provided. If tenant
is provided it will retrieve a scoped token and the service catalog for
the given tenant. Otherwise it will return an unscoped token and without
a service catalog.
'''
c = keystoneclient(request,
username=username,
password=password,
tenant_id=tenant,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
token = c.tokens.authenticate(username=username,
password=password,
tenant=tenant)
return Token(token)
def token_create_scoped(request, tenant, token):
'''
Creates a scoped token using the tenant id and unscoped token; retrieves
the service catalog for the given tenant.
'''
if hasattr(request, '_keystone'):
del request._keystone
c = keystoneclient(request, tenant_id=tenant, token_id=token,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
scoped_token = c.tokens.authenticate(tenant=tenant, token=token)
return Token(scoped_token)
def user_list(request, tenant_id=None):
return [User(u) for u in
keystoneclient(request).users.list(tenant_id=tenant_id)]
def user_create(request, user_id, email, password, tenant_id, enabled):
return User(keystoneclient(request).users.create(
user_id, password, email, tenant_id, enabled))
def user_delete(request, user_id):
keystoneclient(request).users.delete(user_id)
def user_get(request, user_id):
return User(keystoneclient(request).users.get(user_id))
def user_update_email(request, user_id, email):
return User(keystoneclient(request).users.update_email(user_id, email))
def user_update_enabled(request, user_id, enabled):
return User(keystoneclient(request).users.update_enabled(user_id, enabled))
def user_update_password(request, user_id, password):
return User(keystoneclient(request).users \
.update_password(user_id, password))
def user_update_tenant(request, user_id, tenant_id):
return User(keystoneclient(request).users \
.update_tenant(user_id, tenant_id))
def _get_role(request, name):
roles = keystoneclient(request).roles.list()
for role in roles:
if role.name.lower() == name.lower():
return role
raise Exception(_('Role does not exist: %s') % name)
def _get_roleref(request, user_id, tenant_id, role):
rolerefs = keystoneclient(request).roles.get_user_role_refs(user_id)
for roleref in rolerefs:
if roleref.roleId == role.id and roleref.tenantId == tenant_id:
return roleref
raise Exception(_('Role "%s" does not exist for that user on this tenant.')
% role.name)
def role_add_for_tenant_user(request, tenant_id, user_id, role):
role = _get_role(request, role)
return keystoneclient(request).roles.add_user_to_tenant(tenant_id,
user_id,
role.id)
def role_delete_for_tenant_user(request, tenant_id, user_id, role):
role = _get_role(request, role)
roleref = _get_roleref(request, user_id, tenant_id, role)
return keystoneclient(request).roles.remove_user_from_tenant(tenant_id,
user_id,
roleref.id)
def service_get(request, name):
return Services(admin_api(request).services.get(name))
@check_openstackx
def service_list(request):
return [Services(s) for s in admin_api(request).services.list()]
def service_update(request, name, enabled):
return Services(admin_api(request).services.update(name, enabled))

357
horizon/horizon/api/nova.py Normal file
View File

@ -0,0 +1,357 @@
# 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 Nebula, 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 __future__ import absolute_import
import logging
from django.contrib import messages
from novaclient.v1_1 import client as nova_client
from horizon.api.base import *
from horizon.api.deprecated import admin_api
from horizon.api.deprecated import compute_api
from horizon.api.deprecated import check_openstackx
from horizon.api.deprecated import extras_api
from horizon.api.deprecated import REBOOT_HARD
LOG = logging.getLogger(__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 FloatingIp(APIResourceWrapper):
"""Simple wrapper for floating ips"""
_attrs = ['ip', 'fixed_ip', 'instance_id', 'id']
class KeyPair(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.keypairs.Keypair"""
_attrs = ['fingerprint', 'name', 'private_key']
class VirtualInterface(APIResourceWrapper):
_attrs = ['id', 'mac_address']
class Volume(APIResourceWrapper):
"""Nova Volume representation"""
_attrs = ['id', 'status', 'displayName', 'size', 'volumeType', 'createdAt',
'attachments', 'displayDescription']
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', 'image', 'links',
'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
'image_name', 'VirtualInterfaces']
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):
from glance.common import exception as glance_exceptions
from horizon.api import glance
try:
image = glance.image_get(self.request, self.image['id'])
return image.name
except glance_exceptions.NotFound:
return "(not found)"
def reboot(self, hardness=REBOOT_HARD):
compute_api(self.request).servers.reboot(self.id, hardness)
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', 'tenant_id', 'ramdisk_id', 'scheduled_at',
'terminated_at', 'user_data', 'user_id', 'vcpus', 'hostname',
'security_groups']
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 SecurityGroup(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.security_groups.SecurityGroup"""
_attrs = ['id', 'name', 'description', 'tenant_id', 'rules']
class SecurityGroupRule(APIResourceWrapper):
"""Simple wrapper around
openstackx.extras.security_groups.SecurityGroupRule"""
_attrs = ['id', 'parent_group_id', 'group_id', 'ip_protocol',
'from_port', 'to_port', 'groups', 'ip_ranges']
class SecurityGroupRule(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.users.User"""
_attrs = ['id', 'name', 'description', 'tenant_id', 'security_group_rules']
def novaclient(request):
LOG.debug('novaclient connection created using token "%s" and url "%s"' %
(request.user.token, url_for(request, 'compute')))
c = nova_client.Client(username=request.user.username,
api_key=request.user.token,
project_id=request.user.tenant_id,
auth_url=url_for(request, 'compute'))
c.client.auth_token = request.user.token
c.client.management_url = url_for(request, 'compute')
return c
def console_create(request, instance_id, kind='text'):
return Console(extras_api(request).consoles.create(instance_id, kind))
def flavor_create(request, name, memory, vcpu, disk, flavor_id):
# TODO -- convert to novaclient when novaclient adds create support
return Flavor(admin_api(request).flavors.create(
name, int(memory), int(vcpu), int(disk), flavor_id))
def flavor_delete(request, flavor_id, purge=False):
# TODO -- convert to novaclient when novaclient adds delete support
admin_api(request).flavors.delete(flavor_id, purge)
def flavor_get(request, flavor_id):
return Flavor(novaclient(request).flavors.get(flavor_id))
def flavor_list(request):
return [Flavor(f) for f in novaclient(request).flavors.list()]
def tenant_floating_ip_list(request):
"""
Fetches a list of all floating ips.
"""
return [FloatingIp(ip) for ip in novaclient(request).floating_ips.list()]
def tenant_floating_ip_get(request, floating_ip_id):
"""
Fetches a floating ip.
"""
return novaclient(request).floating_ips.get(floating_ip_id)
def tenant_floating_ip_allocate(request):
"""
Allocates a floating ip to tenant.
"""
return novaclient(request).floating_ips.create()
def tenant_floating_ip_release(request, floating_ip_id):
"""
Releases floating ip from the pool of a tenant.
"""
return novaclient(request).floating_ips.delete(floating_ip_id)
def snapshot_create(request, instance_id, name):
return novaclient(request).servers.create_image(instance_id, name)
def keypair_create(request, name):
return KeyPair(novaclient(request).keypairs.create(name))
def keypair_import(request, name, public_key):
return KeyPair(novaclient(request).keypairs.create(name, public_key))
def keypair_delete(request, keypair_id):
novaclient(request).keypairs.delete(keypair_id)
def keypair_list(request):
return [KeyPair(key) for key in novaclient(request).keypairs.list()]
def server_create(request, name, image, flavor,
key_name, user_data, security_groups):
return Server(novaclient(request).servers.create(
name, image, flavor, userdata=user_data,
security_groups=security_groups,
key_name=key_name), request)
def server_delete(request, instance):
compute_api(request).servers.delete(instance)
def server_get(request, instance_id):
return Server(extras_api(request).servers.get(instance_id), request)
@check_openstackx
def server_list(request):
return [Server(s, request) for s in extras_api(request).servers.list()]
@check_openstackx
def admin_server_list(request):
return [Server(s, request) for s in admin_api(request).servers.list()]
def server_reboot(request,
instance_id,
hardness=REBOOT_HARD):
server = server_get(request, instance_id)
server.reboot(hardness)
def server_update(request, instance_id, name, description):
return extras_api(request).servers.update(instance_id,
name=name,
description=description)
def server_add_floating_ip(request, server, address):
"""
Associates floating IP to server's fixed IP.
"""
server = novaclient(request).servers.get(server)
fip = novaclient(request).floating_ips.get(address)
return novaclient(request).servers.add_floating_ip(server, fip)
def server_remove_floating_ip(request, server, address):
"""
Removes relationship between floating and server's fixed ip.
"""
fip = novaclient(request).floating_ips.get(address)
server = novaclient(request).servers.get(fip.instance_id)
return novaclient(request).servers.remove_floating_ip(server, fip)
def tenant_quota_get(request, tenant):
return novaclient(request).quotas.get(tenant)
@check_openstackx
def usage_get(request, tenant_id, start, end):
return Usage(extras_api(request).usage.get(tenant_id, start, end))
@check_openstackx
def usage_list(request, start, end):
return [Usage(u) for u in extras_api(request).usage.list(start, end)]
def security_group_list(request):
return [SecurityGroup(g) for g in novaclient(request).\
security_groups.list()]
def security_group_get(request, security_group_id):
return SecurityGroup(novaclient(request).\
security_groups.get(security_group_id))
def security_group_create(request, name, description):
return SecurityGroup(novaclient(request).\
security_groups.create(name, description))
def security_group_delete(request, security_group_id):
novaclient(request).security_groups.delete(security_group_id)
def security_group_rule_create(request, parent_group_id, ip_protocol=None,
from_port=None, to_port=None, cidr=None,
group_id=None):
return SecurityGroup(novaclient(request).\
security_group_rules.create(parent_group_id,
ip_protocol,
from_port,
to_port,
cidr,
group_id))
def security_group_rule_delete(request, security_group_rule_id):
novaclient(request).security_group_rules.delete(security_group_rule_id)
def volume_list(request):
return [Volume(vol) for vol in novaclient(request).volumes.list()]
def volume_get(request, volume_id):
return Volume(novaclient(request).volumes.get(volume_id))
def volume_instance_list(request, instance_id):
return novaclient(request).volumes.get_server_volumes(instance_id)
def volume_create(request, size, name, description):
return Volume(novaclient(request).volumes.create(
size, name, description))
def volume_delete(request, volume_id):
novaclient(request).volumes.delete(volume_id)
def volume_attach(request, volume_id, instance_id, device):
novaclient(request).volumes.create_server_volume(
instance_id, volume_id, device)
def volume_detach(request, instance_id, attachment_id):
novaclient(request).volumes.delete_server_volume(
instance_id, attachment_id)

View File

@ -0,0 +1,133 @@
# 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 Nebula, 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 __future__ import absolute_import
import logging
from django.conf import settings
from quantum import client as quantum_client
from horizon.api.base import *
from horizon.api.deprecated import extras_api
LOG = logging.getLogger(__name__)
def quantum_api(request):
if hasattr(request, 'user'):
tenant = request.user.tenant_id
else:
tenant = settings.QUANTUM_TENANT
return quantum_client.Client(settings.QUANTUM_URL, settings.QUANTUM_PORT,
False, tenant, 'json')
def quantum_list_networks(request):
return quantum_api(request).list_networks()
def quantum_network_details(request, network_id):
return quantum_api(request).show_network_details(network_id)
def quantum_list_ports(request, network_id):
return quantum_api(request).list_ports(network_id)
def quantum_port_details(request, network_id, port_id):
return quantum_api(request).show_port_details(network_id, port_id)
def quantum_create_network(request, data):
return quantum_api(request).create_network(data)
def quantum_delete_network(request, network_id):
return quantum_api(request).delete_network(network_id)
def quantum_update_network(request, network_id, data):
return quantum_api(request).update_network(network_id, data)
def quantum_create_port(request, network_id):
return quantum_api(request).create_port(network_id)
def quantum_delete_port(request, network_id, port_id):
return quantum_api(request).delete_port(network_id, port_id)
def quantum_attach_port(request, network_id, port_id, data):
return quantum_api(request).attach_resource(network_id, port_id, data)
def quantum_detach_port(request, network_id, port_id):
return quantum_api(request).detach_resource(network_id, port_id)
def quantum_set_port_state(request, network_id, port_id, data):
return quantum_api(request).set_port_state(network_id, port_id, data)
def quantum_port_attachment(request, network_id, port_id):
return quantum_api(request).show_port_attachment(network_id, port_id)
def get_vif_ids(request):
vifs = []
attached_vifs = []
# Get a list of all networks
networks_list = quantum_api(request).list_networks()
for network in networks_list['networks']:
ports = quantum_api(request).list_ports(network['id'])
# Get port attachments
for port in ports['ports']:
port_attachment = quantum_api(request).show_port_attachment(
network['id'],
port['id'])
if port_attachment['attachment']:
attached_vifs.append(
port_attachment['attachment']['id'].encode('ascii'))
# Get all instances
instances = server_list(request)
# Get virtual interface ids by instance
for instance in instances:
id = instance.id
instance_vifs = extras_api(request).virtual_interfaces.list(id)
for vif in instance_vifs:
# Check if this VIF is already connected to any port
if str(vif.id) in attached_vifs:
vifs.append({
'id': vif.id,
'instance': instance.id,
'instance_name': instance.name,
'available': False})
else:
vifs.append({
'id': vif.id,
'instance': instance.id,
'instance_name': instance.name,
'available': True})
return vifs

View File

@ -0,0 +1,132 @@
# 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 Nebula, 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 cloudfiles
from django.conf import settings
from horizon.api.base import *
LOG = logging.getLogger(__name__)
class Container(APIResourceWrapper):
"""Simple wrapper around cloudfiles.container.Container"""
_attrs = ['name']
class SwiftObject(APIResourceWrapper):
_attrs = ['name']
class SwiftAuthentication(object):
"""Auth container to pass CloudFiles storage URL and token from
session.
"""
def __init__(self, storage_url, auth_token):
self.storage_url = storage_url
self.auth_token = auth_token
def authenticate(self):
return (self.storage_url, '', self.auth_token)
def swift_api(request):
LOG.debug('object store connection created using token "%s"'
' and url "%s"' %
(request.session['token'], url_for(request, 'object-store')))
auth = SwiftAuthentication(url_for(request, 'object-store'),
request.session['token'])
return cloudfiles.get_connection(auth=auth)
def swift_container_exists(request, container_name):
try:
swift_api(request).get_container(container_name)
return True
except cloudfiles.errors.NoSuchContainer:
return False
def swift_object_exists(request, container_name, object_name):
container = swift_api(request).get_container(container_name)
try:
container.get_object(object_name)
return True
except cloudfiles.errors.NoSuchObject:
return False
def swift_get_containers(request, marker=None):
return [Container(c) for c in swift_api(request).get_all_containers(
limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000),
marker=marker)]
def swift_create_container(request, name):
if swift_container_exists(request, name):
raise Exception('Container with name %s already exists.' % (name))
return Container(swift_api(request).create_container(name))
def swift_delete_container(request, name):
swift_api(request).delete_container(name)
def swift_get_objects(request, container_name, prefix=None, marker=None):
container = swift_api(request).get_container(container_name)
objects = container.get_objects(prefix=prefix, marker=marker,
limit=getattr(settings, 'SWIFT_PAGINATE_LIMIT', 10000))
return [SwiftObject(o) for o in objects]
def swift_copy_object(request, orig_container_name, orig_object_name,
new_container_name, new_object_name):
container = swift_api(request).get_container(orig_container_name)
if swift_object_exists(request,
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(request, container_name, object_name, object_data):
container = swift_api(request).get_container(container_name)
obj = container.create_object(object_name)
obj.write(object_data)
def swift_delete_object(request, container_name, object_name):
container = swift_api(request).get_container(container_name)
container.delete_object(object_name)
def swift_get_object_data(request, container_name, object_name):
container = swift_api(request).get_container(container_name)
return container.get_object(object_name).stream()

594
horizon/horizon/base.py Normal file
View File

@ -0,0 +1,594 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, 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.
"""
Contains the core classes and functionality that makes Horizon what it is.
This module is considered internal, and should not be relied on directly.
Public APIs are made available through the :mod:`horizon` module and
the classes contained therein.
"""
import copy
import functools
import inspect
from django.conf import settings
from django.conf.urls.defaults import patterns, url, include
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, RegexURLPattern
from django.utils.functional import SimpleLazyObject
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils.translation import ugettext as _
from horizon.decorators import require_roles, _current_component
# Default configuration dictionary. Do not mutate directly. Use copy.copy().
HORIZON_CONFIG = {
# Allow for ordering dashboards; list or tuple if provided.
'dashboards': None,
# Name of a default dashboard; defaults to first alphabetically if None
'default_dashboard': None,
'user_home': None,
}
def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs):
for pattern in urlpatterns:
if getattr(pattern, 'callback', None):
pattern._callback = decorator(pattern.callback, *args, **kwargs)
if getattr(pattern, 'url_patterns', []):
_decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs)
class NotRegistered(Exception):
pass
class HorizonComponent(object):
def __init__(self):
super(HorizonComponent, self).__init__()
if not self.slug:
raise ImproperlyConfigured('Every %s must have a slug.'
% self.__class__)
def __unicode__(self):
return getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__)
def _get_default_urlpatterns(self):
package_string = '.'.join(self.__module__.split('.')[:-1])
if getattr(self, 'urls', None):
try:
mod = import_module('.%s' % self.urls, package_string)
except ImportError:
mod = import_module(self.urls)
urlpatterns = mod.urlpatterns
else:
# Try importing a urls.py from the dashboard package
if module_has_submodule(import_module(package_string), 'urls'):
urls_mod = import_module('.urls', package_string)
urlpatterns = urls_mod.urlpatterns
else:
urlpatterns = patterns('')
return urlpatterns
class Registry(object):
def __init__(self):
self._registry = {}
if not getattr(self, '_registerable_class', None):
raise ImproperlyConfigured('Subclasses of Registry must set a '
'"_registerable_class" property.')
def _register(self, cls):
"""Registers the given class.
If the specified class is already registered then it is ignored.
"""
if not inspect.isclass(cls):
raise ValueError('Only classes may be registered.')
elif not issubclass(cls, self._registerable_class):
raise ValueError('Only %s classes or subclasses may be registered.'
% self._registerable_class)
if cls not in self._registry:
cls._registered_with = self
self._registry[cls] = cls()
return self._registry[cls]
def _unregister(self, cls):
"""Unregisters the given class.
If the specified class isn't registered, ``NotRegistered`` will
be raised.
"""
if not issubclass(cls, self._registerable_class):
raise ValueError('Only %s classes or subclasses may be '
'unregistered.' % self._registerable_class)
if cls not in self._registry.keys():
raise NotRegistered('%s is not registered' % cls)
del self._registry[cls]
return True
def _registered(self, cls):
if inspect.isclass(cls) and issubclass(cls, self._registerable_class):
cls = self._registry.get(cls, None)
if cls:
return cls
else:
# Allow for fetching by slugs as well.
for registered in self._registry.values():
if registered.slug == cls:
return registered
raise NotRegistered('%s with slug "%s" is not registered'
% (self._registerable_class, cls))
class Panel(HorizonComponent):
""" A base class for defining Horizon dashboard panels.
All Horizon dashboard panels should extend from this class. It provides
the appropriate hooks for automatically constructing URLconfs, and
providing role-based access control.
.. attribute:: name
The name of the panel. This will be displayed in the
auto-generated navigation and various other places.
Default: ``''``.
.. attribute:: slug
A unique "short name" for the panel. The slug is used as
a component of the URL path for the panel. Default: ``''``.
.. attribute: roles
A list of role names, all of which a user must possess in order
to access any view associated with this panel. This attribute
is combined cumulatively with any roles required on the
``Dashboard`` class with which it is registered.
.. attribute:: urls
Path to a URLconf of views for this panel using dotted Python
notation. If no value is specified, a file called ``urls.py``
living in the same package as the ``panel.py`` file is used.
Default: ``None``.
.. attribute:: nav
.. method:: nav(context)
The ``nav`` attribute can be either boolean value or a callable
which accepts a ``RequestContext`` object as a single argument
to control whether or not this panel should appear in
automatically-generated navigation. Default: ``True``.
"""
name = ''
slug = ''
urls = None
nav = True
def __repr__(self):
return "<Panel: %s>" % self.__unicode__()
def get_absolute_url(self):
""" Returns the default URL for this panel.
The default URL is defined as the URL pattern with ``name="index"`` in
the URLconf for this panel.
"""
return reverse('horizon:%s:%s:index' % (self._registered_with.slug,
self.slug,))
@property
def _decorated_urls(self):
urlpatterns = self._get_default_urlpatterns()
# Apply access controls to all views in the patterns
roles = getattr(self, 'roles', [])
_decorate_urlconf(urlpatterns, require_roles, roles)
_decorate_urlconf(urlpatterns, _current_component, panel=self)
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.slug, self.slug
class Dashboard(Registry, HorizonComponent):
""" A base class for defining Horizon dashboards.
All Horizon dashboards should extend from this base class. It provides the
appropriate hooks for automatic discovery of :class:`~horizon.Panel`
modules, automatically constructing URLconfs, and providing role-based
access control.
.. attribute:: name
The name of the dashboard. This will be displayed in the
auto-generated navigation and various other places.
Default: ``''``.
.. attribute:: slug
A unique "short name" for the dashboard. The slug is used as
a component of the URL path for the dashboard. Default: ``''``.
.. attribute:: panels
The ``panels`` attribute can be either a list containing the name
of each panel module which should be loaded as part of this
dashboard, or a dictionary of tuples which define groups of
as in the following example::
class Syspanel(horizon.Dashboard):
panels = {'System Panel': ('overview', 'instances', ...)}
Automatically generated navigation will use the order of the
modules in this attribute. Default: ``[]``.
Panel modules must be listed in ``panels`` in order to be
discovered by the automatic registration mechanism.
.. attribute:: default_panel
The name of the panel which should be treated as the default
panel for the dashboard, i.e. when you visit the root URL
for this dashboard, that's the panel that is displayed.
Default: ``None``.
.. attribute: roles
A list of role names, all of which a user must possess in order
to access any panel registered with this dashboard. This attribute
is combined cumulatively with any roles required on individual
:class:`~horizon.Panel` classes.
.. attribute:: urls
Optional path to a URLconf of additional views for this dashboard
which are not connected to specific panels. Default: ``None``.
.. attribute:: nav
Optional boolean to control whether or not this dashboard should
appear in automatically-generated navigation. Default: ``True``.
"""
_registerable_class = Panel
name = ''
slug = ''
urls = None
panels = []
default_panel = None
nav = True
def __repr__(self):
return "<Dashboard: %s>" % self.__unicode__()
def get_panel(self, panel):
"""
Returns the specified :class:`~horizon.Panel` instance registered
with this dashboard.
"""
return self._registered(panel)
def get_panels(self):
"""
Returns the :class:`~horizon.Panel` instances registered with this
dashboard in order.
"""
registered = copy.copy(self._registry)
if type(self.panels) is dict:
panels = {}
for heading, items in self.panels.iteritems():
panels.setdefault(heading, [])
for item in items:
panel = self._registered(item)
panels[heading].append(panel)
registered.pop(panel.__class__)
if len(registered):
panels.setdefault(_("Other"), []).extend(registered.values())
else:
panels = []
for item in self.panels:
panel = self._registered(item)
panels.append(panel)
registered.pop(panel.__class__)
panels.extend(registered.values())
return panels
def get_absolute_url(self):
""" Returns the default URL for this dashboard.
The default URL is defined as the URL pattern with ``name="index"``
in the URLconf for the :class:`~horizon.Panel` specified by
:attr:`~horizon.Dashboard.default_panel`.
"""
return self._registered(self.default_panel).get_absolute_url()
@property
def _decorated_urls(self):
urlpatterns = self._get_default_urlpatterns()
self._autodiscover()
default_panel = None
# Add in each panel's views except for the default view.
for panel in self._registry.values():
if panel.slug == self.default_panel:
default_panel = panel
continue
urlpatterns += patterns('',
url(r'^%s/' % panel.slug, include(panel._decorated_urls)))
# Now the default view, which should come last
if not default_panel:
raise NotRegistered('The default panel "%s" is not registered.'
% self.default_panel)
urlpatterns += patterns('',
url(r'', include(default_panel._decorated_urls)))
# Apply access controls to all views in the patterns
roles = getattr(self, 'roles', [])
_decorate_urlconf(urlpatterns, require_roles, roles)
_decorate_urlconf(urlpatterns, _current_component, dashboard=self)
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.slug, self.slug
def _autodiscover(self):
""" Discovers panels to register from the current dashboard module. """
package = '.'.join(self.__module__.split('.')[:-1])
mod = import_module(package)
panels = []
if type(self.panels) is dict:
[panels.extend(values) for values in self.panels.values()]
else:
panels = self.panels
for panel in panels:
try:
before_import_registry = copy.copy(self._registry)
import_module('.%s.panel' % panel, package)
except:
self._registry = before_import_registry
if module_has_submodule(mod, panel):
raise
@classmethod
def register(cls, panel):
""" Registers a :class:`~horizon.Panel` with this dashboard. """
from horizon import Horizon
return Horizon.register_panel(cls, panel)
@classmethod
def unregister(cls, panel):
""" Unregisters a :class:`~horizon.Panel` from this dashboard. """
from horizon import Horizon
return Horizon.unregister_panel(cls, panel)
class Workflow(object):
def __init__(*args, **kwargs):
raise NotImplementedError()
class LazyURLPattern(SimpleLazyObject):
def __iter__(self):
if self._wrapped is None:
self._setup()
return iter(self._wrapped)
def __reversed__(self):
if self._wrapped is None:
self._setup()
return reversed(self._wrapped)
class Site(Registry, HorizonComponent):
""" The core OpenStack Dashboard class. """
# Required for registry
_registerable_class = Dashboard
name = "Horizon"
namespace = 'horizon'
slug = 'horizon'
urls = 'horizon.site_urls'
def __repr__(self):
return u"<Site: %s>" % self.__unicode__()
@property
def _conf(self):
conf = copy.copy(HORIZON_CONFIG)
conf.update(getattr(settings, 'HORIZON_CONFIG', {}))
return conf
@property
def dashboards(self):
return self._conf['dashboards']
@property
def default_dashboard(self):
return self._conf['default_dashboard']
def register(self, dashboard):
""" Registers a :class:`~horizon.Dashboard` with Horizon."""
return self._register(dashboard)
def unregister(self, dashboard):
""" Unregisters a :class:`~horizon.Dashboard` from Horizon. """
return self._unregister(dashboard)
def registered(self, dashboard):
return self._registered(dashboard)
def register_panel(self, dashboard, panel):
dash_instance = self.registered(dashboard)
return dash_instance._register(panel)
def unregister_panel(self, dashboard, panel):
dash_instance = self.registered(dashboard)
if not dash_instance:
raise NotRegistered("The dashboard %s is not registered."
% dashboard)
return dash_instance._unregister(panel)
def get_dashboard(self, dashboard):
""" Returns the specified :class:`~horizon.Dashboard` instance. """
return self._registered(dashboard)
def get_dashboards(self):
""" Returns an ordered tuple of :class:`~horizon.Dashboard` modules.
Orders dashboards according to the ``"dashboards"`` key in
``settings.HORIZON_CONFIG`` or else returns all registered dashboards
in alphabetical order.
Any remaining :class:`~horizon.Dashboard` classes registered with
Horizon but not listed in ``settings.HORIZON_CONFIG['dashboards']``
will be appended to the end of the list alphabetically.
"""
if self.dashboards:
registered = copy.copy(self._registry)
dashboards = []
for item in self.dashboards:
dashboard = self._registered(item)
dashboards.append(dashboard)
registered.pop(dashboard.__class__)
if len(registered):
extra = registered.values()
extra.sort()
dashboards.extend(extra)
return dashboards
else:
dashboards = self._registry.values()
dashboards.sort()
return dashboards
def get_default_dashboard(self):
""" Returns the default :class:`~horizon.Dashboard` instance.
If ``"default_dashboard"`` is specified in ``settings.HORIZON_CONFIG``
then that dashboard will be returned. If not, the first dashboard
returned by :func:`~horizon.get_dashboards` will be returned.
"""
if self.default_dashboard:
return self._registered(self.default_dashboard)
elif len(self._registry):
return self.get_dashboards()[0]
else:
raise NotRegistered("No dashboard modules have been registered.")
def get_user_home(self, user):
""" Returns the default URL for a particular user.
This method can be used to customize where a user is sent when
they log in, etc. By default it returns the value of
:meth:`get_absolute_url`.
An alternative function can be supplied to customize this behavior
by specifying a either a URL or a function which returns a URL via
the ``"user_home"`` key in ``settings.HORIZON_CONFIG``. Each of these
would be valid::
{"user_home": "/home",} # A URL
{"user_home": "my_module.get_user_home",} # Path to a function
{"user_home": lambda user: "/" + user.name,} # A function
This can be useful if the default dashboard may not be accessible
to all users.
"""
user_home = self._conf['user_home']
if user_home:
if callable(user_home):
return user_home(user)
elif isinstance(user_home, basestring):
# Assume we've got a URL if there's a slash in it
if user_home.find("/") != -1:
return user_home
else:
mod, func = user_home.rsplit(".", 1)
return getattr(import_module(mod), func)(user)
# If it's not callable and not a string, it's wrong.
raise ValueError('The user_home setting must be either a string '
'or a callable object (e.g. a function).')
else:
return self.get_absolute_url()
def get_absolute_url(self):
""" Returns the default URL for Horizon's URLconf.
The default URL is determined by calling
:meth:`~horizon.Dashboard.get_absolute_url`
on the :class:`~horizon.Dashboard` instance returned by
:meth:`~horizon.get_default_dashboard`.
"""
return self.get_default_dashboard().get_absolute_url()
@property
def _lazy_urls(self):
""" Lazy loading for URL patterns.
This method avoids problems associated with attempting to evaluate
the the URLconf before the settings module has been loaded.
"""
def url_patterns():
return self._urls()[0]
return LazyURLPattern(url_patterns), self.namespace, self.slug
def _urls(self):
""" Constructs the URLconf for Horizon from registered Dashboards. """
urlpatterns = self._get_default_urlpatterns()
self._autodiscover()
# Add in each dashboard's views.
for dash in self._registry.values():
urlpatterns += patterns('',
url(r'^%s/' % dash.slug, include(dash._decorated_urls)))
# Return the three arguments to django.conf.urls.defaults.include
return urlpatterns, self.namespace, self.slug
def _autodiscover(self):
""" Discovers modules to register from ``settings.INSTALLED_APPS``.
This makes sure that the appropriate modules get imported to register
themselves with Horizon.
"""
if not getattr(self, '_registerable_class', None):
raise ImproperlyConfigured('You must set a '
'"_registerable_class" property '
'in order to use autodiscovery.')
# Discover both dashboards and panels, in that order
for mod_name in ('dashboard', 'panel'):
for app in settings.INSTALLED_APPS:
mod = import_module(app)
try:
before_import_registry = copy.copy(self._registry)
import_module('%s.%s' % (app, mod_name))
except:
self._registry = before_import_registry
if module_has_submodule(mod, mod_name):
raise
# The one true Horizon
Horizon = Site()

View File

@ -0,0 +1,69 @@
# 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 Nebula, 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.
"""
Context processors used by Horizon.
"""
from django.conf import settings
from django.contrib import messages
from horizon import api
def horizon(request):
""" The main Horizon context processor. Required for Horizon to function.
Adds three variables to the request context:
``tenants``
A list of the tenants the current uses is authorized to access.
``object_store_configured``
Boolean. Will be ``True`` if there is a service of type
``object-store`` in the user's ``ServiceCatalog``.
``network_configured``
Boolean. Will be ``True`` if ``settings.QUANTUM_ENABLED`` is ``True``.
"""
context = {}
# Auth/Keystone context
if request.user.is_authenticated():
try:
tenants = api.tenant_list_for_token(request, request.user.token)
context['tenants'] = tenants
except Exception, e:
if hasattr(request.user, 'message_set'):
messages.error(request, _("Unable to retrieve tenant list from\
keystone: %s") % e.message)
context['tenants'] = []
# Object Store/Swift context
catalog = getattr(request.user, 'service_catalog', [])
object_store = catalog and api.get_service_from_catalog(catalog,
'object-store')
context['object_store_configured'] = object_store
# Quantum context
# TODO(gabriel): Convert to service catalog check when Quantum starts
# supporting keystone integration.
context['network_configured'] = getattr(settings, 'QUANTUM_ENABLED', None)
return context

View File

@ -18,24 +18,45 @@
# 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 cloudfiles.errors import ContainerNotEmpty
from django import shortcuts
from django.shortcuts import render_to_response
from django.utils.translation import ugettext as _
from django.contrib import messages
from django_openstack import api
from django_openstack import forms
from horizon import api
from horizon import forms
LOG = logging.getLogger('django_openstack.dash')
LOG = logging.getLogger(__name__)
class DeleteContainer(forms.SelfHandlingForm):
container_name = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
api.swift_delete_container(request, data['container_name'])
except ContainerNotEmpty, e:
messages.error(request,
_('Unable to delete non-empty container: %s') %
data['container_name'])
LOG.exception('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(request, data['name'])
messages.success(request, _("Container was successfully created."))
return shortcuts.redirect("horizon:nova:containers:index")
class FilterObjects(forms.SelfHandlingForm):
@ -119,77 +140,3 @@ class CopyObject(forms.SelfHandlingForm):
{"container": new_container_name, "obj": new_object_name})
return shortcuts.redirect(request.build_absolute_uri())
@login_required
def index(request, tenant_id, container_name):
marker = request.GET.get('marker', None)
delete_form, handled = DeleteObject.maybe_handle(request)
if handled:
return handled
filter_form, objects = FilterObjects.maybe_handle(request)
if objects is None:
filter_form.fields['container_name'].initial = container_name
objects = api.swift_get_objects(request, container_name, marker=marker)
delete_form.fields['container_name'].initial = container_name
return render_to_response(
'django_openstack/dash/objects/index.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(
'django_openstack/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(
request, 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):
containers = \
[(c.name, c.name) for c in api.swift_get_containers(
request)]
form, handled = CopyObject.maybe_handle(request,
containers=containers)
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(
'django_openstack/dash/objects/copy.html',
{'container_name': container_name,
'object_name': object_name,
'copy_form': form},
context_instance=template.RequestContext(request))

View File

@ -0,0 +1,35 @@
# 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 Nebula, 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.utils.translation import ugettext as _
import horizon
from horizon.dashboards.nova import dashboard
class Containers(horizon.Panel):
name = _("Containers")
slug = 'containers'
def nav(self, context):
return context['object_store_configured']
dashboard.Nova.register(Containers)

View File

@ -20,14 +20,105 @@
import tempfile
from cloudfiles.errors import ContainerNotEmpty
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
from mox import IgnoreArg, IsA
from horizon import api
from horizon import test
class ObjectViewTests(base.BaseViewTests):
CONTAINER_INDEX_URL = reverse('horizon:nova:containers:index')
class ContainerViewTests(test.BaseViewTests):
def setUp(self):
super(ContainerViewTests, self).setUp()
self.container = self.mox.CreateMock(api.Container)
self.container.name = 'containerName'
def test_index(self):
self.mox.StubOutWithMock(api, 'swift_get_containers')
api.swift_get_containers(
IsA(http.HttpRequest), marker=None).AndReturn([self.container])
self.mox.ReplayAll()
res = self.client.get(CONTAINER_INDEX_URL)
self.assertTemplateUsed(res, 'nova/containers/index.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(IsA(http.HttpRequest),
'containerName')
self.mox.ReplayAll()
res = self.client.post(CONTAINER_INDEX_URL, formData)
self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL)
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(
IsA(http.HttpRequest),
'containerName').AndRaise(exception)
self.mox.StubOutWithMock(messages, 'error')
messages.error(IgnoreArg(), IsA(unicode))
self.mox.ReplayAll()
res = self.client.post(CONTAINER_INDEX_URL, formData)
self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL)
self.mox.VerifyAll()
def test_create_container_get(self):
res = self.client.get(reverse('horizon:nova:containers:create'))
self.assertTemplateUsed(res, 'nova/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(
IsA(http.HttpRequest), 'CreateContainer')
self.mox.StubOutWithMock(messages, 'success')
messages.success(IgnoreArg(), IsA(basestring))
res = self.client.post(reverse('horizon:nova:containers:create'),
formData)
self.assertRedirectsNoFollow(res, CONTAINER_INDEX_URL)
class ObjectViewTests(test.BaseViewTests):
CONTAINER_NAME = 'containerName'
def setUp(self):
@ -44,22 +135,18 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.get(reverse('dash_objects',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]))
self.assertTemplateUsed(res,
'django_openstack/dash/objects/index.html')
res = self.client.get(reverse('horizon:nova:containers:object_index',
args=[self.CONTAINER_NAME]))
self.assertTemplateUsed(res, 'nova/objects/index.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]))
res = self.client.get(reverse('horizon:nova:containers:object_upload',
args=[self.CONTAINER_NAME]))
self.assertTemplateUsed(res,
'django_openstack/dash/objects/upload.html')
self.assertTemplateUsed(res, 'nova/objects/upload.html')
def test_upload(self):
OBJECT_DATA = 'objectData'
@ -82,14 +169,13 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.post(reverse('dash_objects_upload',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]),
res = self.client.post(reverse('horizon:nova:containers:object_upload',
args=[self.CONTAINER_NAME]),
formData)
self.assertRedirectsNoFollow(res, reverse('dash_objects_upload',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:containers:object_upload',
args=[self.CONTAINER_NAME]))
self.mox.VerifyAll()
@ -106,14 +192,13 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.post(reverse('dash_objects',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]),
res = self.client.post(reverse('horizon:nova:containers:object_index',
args=[self.CONTAINER_NAME]),
formData)
self.assertRedirectsNoFollow(res, reverse('dash_objects',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:containers:object_index',
args=[self.CONTAINER_NAME]))
self.mox.VerifyAll()
@ -128,10 +213,9 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.get(reverse('dash_objects_download',
args=[self.TEST_TENANT,
self.CONTAINER_NAME,
OBJECT_NAME]))
res = self.client.get(reverse(
'horizon:nova:containers:object_download',
args=[self.CONTAINER_NAME, OBJECT_NAME]))
self.assertEqual(res.content, OBJECT_DATA)
self.assertTrue(res.has_header('Content-Disposition'))
@ -150,13 +234,11 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.get(reverse('dash_object_copy',
args=[self.TEST_TENANT,
self.CONTAINER_NAME,
res = self.client.get(reverse('horizon:nova:containers:object_copy',
args=[self.CONTAINER_NAME,
OBJECT_NAME]))
self.assertTemplateUsed(res,
'django_openstack/dash/objects/copy.html')
self.assertTemplateUsed(res, 'nova/objects/copy.html')
self.mox.VerifyAll()
@ -188,16 +270,15 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.ReplayAll()
res = self.client.post(reverse('dash_object_copy',
args=[self.TEST_TENANT,
ORIG_CONTAINER_NAME,
res = self.client.post(reverse('horizon:nova:containers:object_copy',
args=[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.assertRedirectsNoFollow(res,
reverse('horizon:nova:containers:object_copy',
args=[ORIG_CONTAINER_NAME,
ORIG_OBJECT_NAME]))
self.mox.VerifyAll()
@ -212,17 +293,15 @@ class ObjectViewTests(base.BaseViewTests):
self.mox.StubOutWithMock(api, 'swift_get_objects')
api.swift_get_objects(IsA(http.HttpRequest),
unicode(self.CONTAINER_NAME),
prefix=unicode(PREFIX)
).AndReturn(self.swift_objects)
prefix=unicode(PREFIX))\
.AndReturn(self.swift_objects)
self.mox.ReplayAll()
res = self.client.post(reverse('dash_objects',
args=[self.TEST_TENANT,
self.CONTAINER_NAME]),
res = self.client.post(reverse('horizon:nova:containers:object_index',
args=[self.CONTAINER_NAME]),
formData)
self.assertTemplateUsed(res,
'django_openstack/dash/objects/index.html')
self.assertTemplateUsed(res, 'nova/objects/index.html')
self.mox.VerifyAll()

View File

@ -18,31 +18,19 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from django.conf.urls.defaults import patterns, url
def time():
'''Overrideable version of datetime.datetime.today'''
if time.override_time:
return time.override_time
return datetime.time()
time.override_time = None
OBJECTS = r'^(?P<container_name>[^/]+)/%s$'
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:
return utcnow.override_time
return datetime.datetime.utcnow()
utcnow.override_time = None
# Swift containers and objects.
urlpatterns = patterns('horizon.dashboards.nova.containers.views',
url(r'^$', 'index', name='index'),
url(r'^create/$', 'create', name='create'),
url(OBJECTS % '', 'object_index', name='object_index'),
url(OBJECTS % 'upload', 'object_upload', name='object_upload'),
url(OBJECTS % '(?P<object_name>[^/]+)/copy',
'object_copy', name='object_copy'),
url(OBJECTS % '(?P<object_name>[^/]+)/download',
'object_download', name='object_download'))

View File

@ -0,0 +1,134 @@
# 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 Nebula, 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.contrib.auth.decorators import login_required
from django import shortcuts
from django.utils.translation import ugettext as _
from horizon import api
from horizon.dashboards.nova.containers.forms import (DeleteContainer,
CreateContainer, FilterObjects, DeleteObject, UploadObject, CopyObject)
LOG = logging.getLogger(__name__)
@login_required
def index(request):
marker = request.GET.get('marker', None)
delete_form, handled = DeleteContainer.maybe_handle(request)
if handled:
return handled
containers = api.swift_get_containers(request, marker=marker)
return shortcuts.render(request,
'nova/containers/index.html',
{'containers': containers,
'delete_form': delete_form})
@login_required
def create(request):
form, handled = CreateContainer.maybe_handle(request)
if handled:
return handled
return shortcuts.render(request,
'nova/containers/create.html',
{'create_form': form})
@login_required
def object_index(request, container_name):
marker = request.GET.get('marker', None)
delete_form, handled = DeleteObject.maybe_handle(request)
if handled:
return handled
filter_form, objects = FilterObjects.maybe_handle(request)
if objects is None:
filter_form.fields['container_name'].initial = container_name
objects = api.swift_get_objects(request, container_name, marker=marker)
delete_form.fields['container_name'].initial = container_name
return shortcuts.render(request,
'nova/objects/index.html',
{'container_name': container_name,
'objects': objects,
'delete_form': delete_form,
'filter_form': filter_form})
@login_required
def object_upload(request, container_name):
form, handled = UploadObject.maybe_handle(request)
if handled:
return handled
form.fields['container_name'].initial = container_name
return shortcuts.render(request,
'nova/objects/upload.html',
{'container_name': container_name,
'upload_form': form})
@login_required
def object_download(request, container_name, object_name):
object_data = api.swift_get_object_data(
request, 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 object_copy(request, container_name, object_name):
containers = \
[(c.name, c.name) for c in api.swift_get_containers(
request)]
form, handled = CopyObject.maybe_handle(request,
containers=containers)
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 shortcuts.render(request,
'nova/objects/copy.html',
{'container_name': container_name,
'object_name': object_name,
'copy_form': form})

View File

@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, 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.utils.translation import ugettext as _
import horizon
class Nova(horizon.Dashboard):
name = "User Dashboard"
slug = "nova"
panels = {_("Manage Compute"): ('overview', 'instances', 'images',
'snapshots', 'keypairs', 'volumes',
'floating_ips', 'security_groups',),
_("Network"): ('networks',),
_("Object Store"): ('containers',)}
default_panel = 'overview'
horizon.register(Nova)

View File

@ -18,23 +18,19 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
Views for managing Nova floating ips.
"""
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.utils.translation import ugettext as _
from django_openstack import api
from django_openstack import forms
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import forms
LOG = logging.getLogger('django_openstack.dash.views.floating_ip')
LOG = logging.getLogger(__name__)
class ReleaseFloatingIp(forms.SelfHandlingForm):
@ -81,7 +77,7 @@ class FloatingIpAssociate(forms.SelfHandlingForm):
LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error associating Floating IP: %s')
% e.message)
return shortcuts.redirect('dash_floating_ips', request.user.tenant_id)
return shortcuts.redirect('horizon:nova:floating_ips:index')
class FloatingIpDisassociate(forms.SelfHandlingForm):
@ -102,7 +98,7 @@ class FloatingIpDisassociate(forms.SelfHandlingForm):
LOG.exception("ClientException in FloatingIpAssociate")
messages.error(request, _('Error disassociating Floating IP: %s')
% e.message)
return shortcuts.redirect('dash_floating_ips', request.user.tenant_id)
return shortcuts.redirect('horizon:nova:floating_ips:index')
class FloatingIpAllocate(forms.SelfHandlingForm):
@ -124,58 +120,4 @@ class FloatingIpAllocate(forms.SelfHandlingForm):
messages.error(request, _('Error allocating Floating IP "%(ip)s"\
to tenant "%(tenant)s": %(msg)s') %
{"ip": fip.ip, "tenant": data['tenant_id'], "msg": e.message})
return shortcuts.redirect('dash_floating_ips', request.user.tenant_id)
@login_required
def index(request, tenant_id):
for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate):
_unused, handled = f.maybe_handle(request)
if handled:
return handled
try:
floating_ips = api.tenant_floating_ip_list(request)
except novaclient_exceptions.ClientException, e:
floating_ips = []
LOG.exception("ClientException in floating ip index")
messages.error(request,
_('Error fetching floating ips: %s') % e.message)
return shortcuts.render_to_response(
'django_openstack/dash/floating_ips/index.html', {
'allocate_form': FloatingIpAllocate(
initial={'tenant_id': request.user.tenant_id}),
'disassociate_form': FloatingIpDisassociate(),
'floating_ips': floating_ips,
'release_form': ReleaseFloatingIp(),
}, context_instance=template.RequestContext(request))
@login_required
def associate(request, tenant_id, ip_id):
instancelist = [(server.id, 'id: %s, name: %s' %
(server.id, server.name))
for server in api.server_list(request)]
form, handled = FloatingIpAssociate().maybe_handle(request, initial={
'floating_ip_id': ip_id,
'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip,
'instances': instancelist})
if handled:
return handled
return shortcuts.render_to_response(
'django_openstack/dash/floating_ips/associate.html', {
'associate_form': form,
}, context_instance=template.RequestContext(request))
@login_required
def disassociate(request, tenant_id, ip_id):
form, handled = FloatingIpDisassociate().maybe_handle(request)
if handled:
return handled
return shortcuts.render_to_response(
'django_openstack/dash/floating_ips/associate.html', {
}, context_instance=template.RequestContext(request))
return shortcuts.redirect('horizon:nova:floating_ips:index')

View File

@ -0,0 +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 Nebula, 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 horizon
from horizon.dashboards.nova import dashboard
class FloatingIPs(horizon.Panel):
name = "Floating IPs"
slug = 'floating_ips'
dashboard.Nova.register(FloatingIPs)

View File

@ -24,15 +24,18 @@ from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django_openstack import api
from django_openstack import utils
from django_openstack.dash.views.floating_ips import FloatingIpAssociate
from django_openstack.tests.view_tests import base
from mox import IsA, IgnoreArg
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import test
from horizon.dashboards.nova.floating_ips.forms import FloatingIpAssociate
class FloatingIpViewTests(base.BaseViewTests):
FLOATING_IPS_INDEX = reverse('horizon:nova:floating_ips:index')
class FloatingIpViewTests(test.BaseViewTests):
def setUp(self):
super(FloatingIpViewTests, self).setUp()
@ -57,10 +60,8 @@ class FloatingIpViewTests(base.BaseViewTests):
AndReturn(self.floating_ips)
self.mox.ReplayAll()
res = self.client.get(reverse('dash_floating_ips',
args=[self.TEST_TENANT]))
self.assertTemplateUsed(res,
'django_openstack/dash/floating_ips/index.html')
res = self.client.get(FLOATING_IPS_INDEX)
self.assertTemplateUsed(res, 'nova/floating_ips/index.html')
self.assertItemsEqual(res.context['floating_ips'], self.floating_ips)
self.mox.VerifyAll()
@ -76,10 +77,9 @@ class FloatingIpViewTests(base.BaseViewTests):
AndReturn(self.floating_ip)
self.mox.ReplayAll()
res = self.client.get(reverse('dash_floating_ips_associate',
args=[self.TEST_TENANT, 1]))
self.assertTemplateUsed(res,
'django_openstack/dash/floating_ips/associate.html')
res = self.client.get(reverse('horizon:nova:floating_ips:associate',
args=[1]))
self.assertTemplateUsed(res, 'nova/floating_ips/associate.html')
self.mox.VerifyAll()
def test_associate_post(self):
@ -107,15 +107,14 @@ class FloatingIpViewTests(base.BaseViewTests):
AndReturn(self.floating_ip)
self.mox.ReplayAll()
res = self.client.post(reverse('dash_floating_ips_associate',
args=[self.TEST_TENANT, 1]),
{'instance_id': 1,
'floating_ip_id': self.floating_ip.id,
'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'})
res = self.client.post(reverse('horizon:nova:floating_ips:associate',
args=[1]),
{'instance_id': 1,
'floating_ip_id': self.floating_ip.id,
'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'})
self.assertRedirects(res, reverse('dash_floating_ips',
args=[self.TEST_TENANT]))
self.assertRedirects(res, FLOATING_IPS_INDEX)
self.mox.VerifyAll()
def test_associate_post_with_exception(self):
@ -146,24 +145,22 @@ class FloatingIpViewTests(base.BaseViewTests):
AndReturn(self.floating_ip)
self.mox.ReplayAll()
res = self.client.post(reverse('dash_floating_ips_associate',
args=[self.TEST_TENANT, 1]),
{'instance_id': 1,
'floating_ip_id': self.floating_ip.id,
'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'})
res = self.client.post(reverse('horizon:nova:floating_ips:associate',
args=[1]),
{'instance_id': 1,
'floating_ip_id': self.floating_ip.id,
'floating_ip': self.floating_ip.ip,
'method': 'FloatingIpAssociate'})
self.assertRaises(novaclient_exceptions.ClientException)
self.assertRedirects(res, reverse('dash_floating_ips',
args=[self.TEST_TENANT]))
self.assertRedirects(res, FLOATING_IPS_INDEX)
self.mox.VerifyAll()
def test_disassociate(self):
res = self.client.get(reverse('dash_floating_ips_disassociate',
args=[self.TEST_TENANT, 1]))
self.assertTemplateUsed(res,
'django_openstack/dash/floating_ips/associate.html')
res = self.client.get(reverse('horizon:nova:floating_ips:disassociate',
args=[1]))
self.assertTemplateUsed(res, 'nova/floating_ips/associate.html')
self.mox.VerifyAll()
def test_disassociate_post(self):
@ -184,12 +181,11 @@ class FloatingIpViewTests(base.BaseViewTests):
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
AndReturn(self.floating_ip)
self.mox.ReplayAll()
res = self.client.post(reverse('dash_floating_ips_disassociate',
args=[self.TEST_TENANT, 1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
self.assertRedirects(res, reverse('dash_floating_ips',
args=[self.TEST_TENANT]))
res = self.client.post(
reverse('horizon:nova:floating_ips:disassociate', args=[1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
self.assertRedirects(res, FLOATING_IPS_INDEX)
self.mox.VerifyAll()
def test_disassociate_post_with_exception(self):
@ -211,11 +207,10 @@ class FloatingIpViewTests(base.BaseViewTests):
api.tenant_floating_ip_get(IsA(http.HttpRequest), IsA(unicode)).\
AndReturn(self.floating_ip)
self.mox.ReplayAll()
res = self.client.post(reverse('dash_floating_ips_disassociate',
args=[self.TEST_TENANT, 1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
res = self.client.post(
reverse('horizon:nova:floating_ips:disassociate', args=[1]),
{'floating_ip_id': self.floating_ip.id,
'method': 'FloatingIpDisassociate'})
self.assertRaises(novaclient_exceptions.ClientException)
self.assertRedirects(res, reverse('dash_floating_ips',
args=[self.TEST_TENANT]))
self.assertRedirects(res, FLOATING_IPS_INDEX)
self.mox.VerifyAll()

View File

@ -0,0 +1,28 @@
# 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 Nebula, 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 patterns, url
urlpatterns = patterns('horizon.dashboards.nova.floating_ips.views',
url(r'^$', 'index', name='index'),
url(r'^(?P<ip_id>[^/]+)/associate/$', 'associate', name='associate'),
url(r'^(?P<ip_id>[^/]+)/disassociate/$', 'disassociate',
name='disassociate'))

View File

@ -0,0 +1,88 @@
# 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 Nebula, 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 Nova floating IPs.
"""
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.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon.dashboards.nova.floating_ips.forms import (ReleaseFloatingIp,
FloatingIpAssociate, FloatingIpDisassociate, FloatingIpAllocate)
LOG = logging.getLogger(__name__)
@login_required
def index(request):
for f in (ReleaseFloatingIp, FloatingIpDisassociate, FloatingIpAllocate):
_unused, handled = f.maybe_handle(request)
if handled:
return handled
try:
floating_ips = api.tenant_floating_ip_list(request)
except novaclient_exceptions.ClientException, e:
floating_ips = []
LOG.exception("ClientException in floating ip index")
messages.error(request,
_('Error fetching floating ips: %s') % e.message)
allocate_form = FloatingIpAllocate(initial={
'tenant_id': request.user.tenant_id})
return shortcuts.render(request,
'nova/floating_ips/index.html', {
'allocate_form': allocate_form,
'disassociate_form': FloatingIpDisassociate(),
'floating_ips': floating_ips,
'release_form': ReleaseFloatingIp()})
@login_required
def associate(request, ip_id):
instancelist = [(server.id, 'id: %s, name: %s' %
(server.id, server.name))
for server in api.server_list(request)]
form, handled = FloatingIpAssociate().maybe_handle(request, initial={
'floating_ip_id': ip_id,
'floating_ip': api.tenant_floating_ip_get(request, ip_id).ip,
'instances': instancelist})
if handled:
return handled
return shortcuts.render(request,
'nova/floating_ips/associate.html', {
'associate_form': form})
@login_required
def disassociate(request, ip_id):
form, handled = FloatingIpDisassociate().maybe_handle(request)
if handled:
return handled
return shortcuts.render(request, 'nova/floating_ips/associate.html', {})

View File

@ -24,21 +24,17 @@ Views for managing Nova images.
import logging
from django import template
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render_to_response
from django.shortcuts import redirect
from django.utils.text import normalize_newlines
from django.utils.translation import ugettext as _
from django_openstack import api
from django_openstack import forms
from openstackx.api import exceptions as api_exceptions
from glance.common import exception as glance_exception
from novaclient import exceptions as novaclient_exceptions
from openstackx.api import exceptions as api_exceptions
from horizon import api
from horizon import forms
LOG = logging.getLogger('django_openstack.dash.views.images')
LOG = logging.getLogger(__name__)
class UpdateImageForm(forms.SelfHandlingForm):
@ -169,7 +165,7 @@ class LaunchForm(forms.SelfHandlingForm):
msg = _('Instance was successfully launched')
LOG.info(msg)
messages.success(request, msg)
return redirect('dash_instances', tenant_id)
return redirect('horizon:nova:instances:index')
except api_exceptions.ApiException, e:
LOG.exception('ApiException while creating instances of image "%s"'
@ -202,129 +198,3 @@ class DeleteImage(forms.SelfHandlingForm):
_("Error deleting image: %(image)s: %i(msg)s")
% {"image": image_id, "msg": e.message})
return redirect(request.build_absolute_uri())
@login_required
def index(request, tenant_id):
for f in (DeleteImage, ):
unused, handled = f.maybe_handle(request)
if handled:
return handled
delete_form = DeleteImage()
all_images = []
try:
all_images = api.image_list_detailed(request)
if not all_images:
messages.info(request, _("There are currently no images."))
except glance_exception.ClientConnectionError, e:
LOG.exception("Error connecting to glance")
messages.error(request, _("Error connecting to glance: %s") % str(e))
except glance_exception.Error, e:
LOG.exception("Error retrieving image list")
messages.error(request, _("Error retrieving image list: %s") % str(e))
except api_exceptions.ApiException, e:
msg = _("Unable to retreive image info from glance: %s") % str(e)
LOG.exception(msg)
messages.error(request, msg)
images = [im for im in all_images
if im['container_format'] not in ['aki', 'ari']]
return render_to_response(
'django_openstack/dash/images/index.html', {
'delete_form': delete_form,
'images': images,
}, context_instance=template.RequestContext(request))
@login_required
def launch(request, tenant_id, image_id):
def flavorlist():
try:
fl = api.flavor_list(request)
# TODO add vcpu count to flavors
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 api_exceptions.ApiException:
LOG.exception('Unable to retrieve list of instance types')
return [(1, 'm1.tiny')]
def keynamelist():
try:
fl = api.keypair_list(request)
sel = [(f.name, f.name) for f in fl]
return sel
except api_exceptions.ApiException:
LOG.exception('Unable to retrieve list of keypairs')
return []
def securitygrouplist():
try:
fl = api.security_group_list(request)
sel = [(f.name, f.name) for f in fl]
return sel
except novaclient_exceptions.ClientException, e:
LOG.exception('Unable to retrieve list of security groups')
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
image = api.image_get(request, image_id)
quotas = api.tenant_quota_get(request, request.user.tenant_id)
try:
quotas.ram = int(quotas.ram)
except Exception, e:
messages.error(request,
_('Error parsing quota for %(image)s: %(msg)s') %
{"image": image_id, "msg": e.message})
return redirect('dash_instances', tenant_id)
form, handled = LaunchForm.maybe_handle(
request, initial={'flavorlist': flavorlist(),
'keynamelist': keynamelist(),
'securitygrouplist': securitygrouplist(),
'image_id': image_id,
'tenant_id': tenant_id})
if handled:
return handled
return render_to_response(
'django_openstack/dash/images/launch.html', {
'image': image,
'form': form,
'quotas': quotas,
}, context_instance=template.RequestContext(request))
@login_required
def update(request, tenant_id, image_id):
try:
image = api.image_get(request, image_id)
except glance_exception.ClientConnectionError, e:
LOG.exception("Error connecting to glance")
messages.error(request, _("Error connecting to glance: %s")
% e.message)
except glance_exception.Error, e:
LOG.exception('Error retrieving image with id "%s"' % image_id)
messages.error(request,
_("Error retrieving image %(image)s: %(msg)s")
% {"image": image_id, "msg": e.message})
form, handled = UpdateImageForm().maybe_handle(request, initial={
'image_id': image_id,
'name': image.get('name', ''),
'kernel': image['properties'].get('kernel_id', ''),
'ramdisk': image['properties'].get('ramdisk_id', ''),
'architecture': image['properties'].get('architecture', ''),
'container_format': image.get('container_format', ''),
'disk_format': image.get('disk_format', ''), })
if handled:
return handled
return render_to_response('django_openstack/dash/images/update.html', {
'form': form,
}, context_instance=template.RequestContext(request))

View File

@ -18,12 +18,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from django import forms
import horizon
from horizon.dashboards.nova import dashboard
class DisableProject(forms.Form):
project_name = forms.CharField()
class Images(horizon.Panel):
name = "Images"
slug = 'images'
class DisableIpAddress(forms.Form):
cidr = forms.CharField()
dashboard.Nova.register(Images)

Some files were not shown because too many files have changed in this diff Show More