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:
parent
91ceb59bae
commit
9742842795
16
.bzrignore
16
.bzrignore
@ -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
23
.gitignore
vendored
@ -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
34
README
@ -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
|
||||
|
@ -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
@ -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}
|
@ -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'),
|
||||
)
|
@ -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))
|
@ -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}
|
@ -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
|
@ -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
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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)
|
@ -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'),
|
||||
)
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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"%} >></a>
|
||||
{% endblock %}
|
@ -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> »
|
||||
<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"%} >></a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -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>
|
@ -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 %}
|
@ -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 {}
|
@ -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}
|
@ -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
@ -1,2 +0,0 @@
|
||||
Intentionally not a python module so that test runner won't find
|
||||
these broken tests
|
@ -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]
|
@ -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()
|
@ -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()
|
@ -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()
|
@ -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)
|
@ -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, '/')
|
@ -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)
|
@ -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()
|
@ -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'])
|
@ -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()
|
@ -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)
|
@ -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]))
|
@ -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)
|
@ -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()
|
@ -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`
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
37
docs/source/faq.rst
Normal 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
19
docs/source/glossary.rst
Normal 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
101
docs/source/index.rst
Normal 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
124
docs/source/intro.rst
Normal 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
146
docs/source/quickstart.rst
Normal 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.
|
6
docs/source/ref/context_processors.rst
Normal file
6
docs/source/ref/context_processors.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==========================
|
||||
Horizon Context Processors
|
||||
==========================
|
||||
|
||||
.. automodule:: horizon.context_processors
|
||||
:members:
|
6
docs/source/ref/decorators.rst
Normal file
6
docs/source/ref/decorators.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==================
|
||||
Horizon Decorators
|
||||
==================
|
||||
|
||||
.. automodule:: horizon.decorators
|
||||
:members:
|
6
docs/source/ref/exceptions.rst
Normal file
6
docs/source/ref/exceptions.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==================
|
||||
Horizon Exceptions
|
||||
==================
|
||||
|
||||
.. automodule:: horizon.exceptions
|
||||
:members:
|
17
docs/source/ref/forms.rst
Normal file
17
docs/source/ref/forms.rst
Normal 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:
|
42
docs/source/ref/horizon.rst
Normal file
42
docs/source/ref/horizon.rst
Normal 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:
|
6
docs/source/ref/middleware.rst
Normal file
6
docs/source/ref/middleware.rst
Normal file
@ -0,0 +1,6 @@
|
||||
==================
|
||||
Horizon Middleware
|
||||
==================
|
||||
|
||||
.. automodule:: horizon.middleware
|
||||
:members:
|
100
docs/source/ref/run_tests.rst
Normal file
100
docs/source/ref/run_tests.rst
Normal 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.
|
6
docs/source/ref/users.rst
Normal file
6
docs/source/ref/users.rst
Normal file
@ -0,0 +1,6 @@
|
||||
=================
|
||||
Horizon User APIs
|
||||
=================
|
||||
|
||||
.. automodule:: horizon.users
|
||||
:members:
|
12
docs/source/ref/views.rst
Normal file
12
docs/source/ref/views.rst
Normal 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
62
docs/source/testing.rst
Normal 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.
|
@ -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
59
horizon/README
Normal 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
|
@ -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
|
50
horizon/horizon/__init__.py
Normal file
50
horizon/horizon/__init__.py
Normal 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
|
39
horizon/horizon/api/__init__.py
Normal file
39
horizon/horizon/api/__init__.py
Normal 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
118
horizon/horizon/api/base.py
Normal 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)
|
95
horizon/horizon/api/deprecated.py
Normal file
95
horizon/horizon/api/deprecated.py
Normal 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)
|
89
horizon/horizon/api/glance.py
Normal file
89
horizon/horizon/api/glance.py
Normal 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)]
|
256
horizon/horizon/api/keystone.py
Normal file
256
horizon/horizon/api/keystone.py
Normal 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
357
horizon/horizon/api/nova.py
Normal 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)
|
133
horizon/horizon/api/quantum.py
Normal file
133
horizon/horizon/api/quantum.py
Normal 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
|
132
horizon/horizon/api/swift.py
Normal file
132
horizon/horizon/api/swift.py
Normal 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
594
horizon/horizon/base.py
Normal 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()
|
69
horizon/horizon/context_processors.py
Normal file
69
horizon/horizon/context_processors.py
Normal 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
|
@ -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))
|
35
horizon/horizon/dashboards/nova/containers/panel.py
Normal file
35
horizon/horizon/dashboards/nova/containers/panel.py
Normal 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)
|
@ -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()
|
@ -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'))
|
134
horizon/horizon/dashboards/nova/containers/views.py
Normal file
134
horizon/horizon/dashboards/nova/containers/views.py
Normal 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})
|
33
horizon/horizon/dashboards/nova/dashboard.py
Normal file
33
horizon/horizon/dashboards/nova/dashboard.py
Normal 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)
|
@ -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')
|
30
horizon/horizon/dashboards/nova/floating_ips/panel.py
Normal file
30
horizon/horizon/dashboards/nova/floating_ips/panel.py
Normal 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)
|
@ -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()
|
28
horizon/horizon/dashboards/nova/floating_ips/urls.py
Normal file
28
horizon/horizon/dashboards/nova/floating_ips/urls.py
Normal 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'))
|
88
horizon/horizon/dashboards/nova/floating_ips/views.py
Normal file
88
horizon/horizon/dashboards/nova/floating_ips/views.py
Normal 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', {})
|
@ -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))
|
@ -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
Loading…
Reference in New Issue
Block a user