diff --git a/.gitignore b/.gitignore index d353785ecb..daec31fc9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.swp +*.sqlite3 .environment_version .selenium_log .coverage* diff --git a/horizon/api/__init__.py b/horizon/api/__init__.py index 3e37214dff..b2b1d24375 100644 --- a/horizon/api/__init__.py +++ b/horizon/api/__init__.py @@ -36,3 +36,4 @@ 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 * diff --git a/horizon/api/nova.py b/horizon/api/nova.py index e4315a641a..ef367680fa 100644 --- a/horizon/api/nova.py +++ b/horizon/api/nova.py @@ -292,11 +292,13 @@ def keypair_list(request): def server_create(request, name, image, flavor, key_name, user_data, - security_groups, block_device_mapping, instance_count=1): + security_groups, block_device_mapping, nics=None, + instance_count=1): return Server(novaclient(request).servers.create( name, image, flavor, userdata=user_data, security_groups=security_groups, key_name=key_name, block_device_mapping=block_device_mapping, + nics=nics, min_count=instance_count), request) diff --git a/horizon/api/quantum.py b/horizon/api/quantum.py new file mode 100644 index 0000000000..4574257320 --- /dev/null +++ b/horizon/api/quantum.py @@ -0,0 +1,239 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Cisco Systems, Inc. +# Copyright 2012 NEC Corporation +# +# 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 quantumclient.v2_0 import client as quantum_client +from django.utils.datastructures import SortedDict + +from horizon.api.base import APIDictWrapper, url_for + + +LOG = logging.getLogger(__name__) + + +class QuantumAPIDictWrapper(APIDictWrapper): + + def set_id_as_name_if_empty(self, length=8): + try: + if not self._apidict['name']: + id = self._apidict['id'] + if length: + id = id[:length] + self._apidict['name'] = '(%s)' % id + except KeyError: + pass + + def items(self): + return self._apidict.items() + + +class Network(QuantumAPIDictWrapper): + """Wrapper for quantum Networks""" + _attrs = ['name', 'id', 'subnets', 'tenant_id', 'status', 'admin_state_up'] + + def __init__(self, apiresource): + apiresource['admin_state'] = \ + 'UP' if apiresource['admin_state_up'] else 'DOWN' + super(Network, self).__init__(apiresource) + + +class Subnet(QuantumAPIDictWrapper): + """Wrapper for quantum subnets""" + _attrs = ['name', 'id', 'cidr', 'network_id', 'tenant_id', + 'ip_version', 'ipver_str'] + + def __init__(self, apiresource): + apiresource['ipver_str'] = get_ipver_str(apiresource['ip_version']) + super(Subnet, self).__init__(apiresource) + + +class Port(QuantumAPIDictWrapper): + """Wrapper for quantum ports""" + _attrs = ['name', 'id', 'network_id', 'tenant_id', + 'admin_state_up', 'status', 'mac_address', + 'fixed_ips', 'host_routes', 'device_id'] + + def __init__(self, apiresource): + apiresource['admin_state'] = \ + 'UP' if apiresource['admin_state_up'] else 'DOWN' + super(Port, self).__init__(apiresource) + + +IP_VERSION_DICT = {4: 'IPv4', 6: 'IPv6'} + + +def get_ipver_str(ip_version): + """Convert an ip version number to a human-friendly string""" + return IP_VERSION_DICT.get(ip_version, '') + + +def quantumclient(request): + LOG.debug('quantumclient connection created using token "%s" and url "%s"' + % (request.user.token.id, url_for(request, 'network'))) + LOG.debug('user_id=%(user)s, tenant_id=%(tenant)s' % + {'user': request.user.id, 'tenant': request.user.tenant_id}) + c = quantum_client.Client(token=request.user.token.id, + endpoint_url=url_for(request, 'network')) + return c + + +def network_list(request, **params): + LOG.debug("network_list(): params=%s" % (params)) + networks = quantumclient(request).list_networks(**params).get('networks') + # Get subnet list to expand subnet info in network list. + subnets = subnet_list(request) + subnet_dict = SortedDict([(s['id'], s) for s in subnets]) + # Expand subnet list from subnet_id to values. + for n in networks: + n['subnets'] = [subnet_dict[s] for s in n['subnets']] + return [Network(n) for n in networks] + + +def network_get(request, network_id, **params): + LOG.debug("network_get(): netid=%s, params=%s" % (network_id, params)) + network = quantumclient(request).show_network(network_id, + **params).get('network') + # Since the number of subnets per network must be small, + # call subnet_get() for each subnet instead of calling + # subnet_list() once. + network['subnets'] = [subnet_get(request, sid) + for sid in network['subnets']] + return Network(network) + + +def network_create(request, **kwargs): + """ + Create a subnet on a specified network. + :param request: request context + :param tenant_id: (optional) tenant id of the network created + :param name: (optional) name of the network created + :returns: Subnet object + """ + LOG.debug("network_create(): kwargs = %s" % kwargs) + body = {'network': kwargs} + network = quantumclient(request).create_network(body=body).get('network') + return Network(network) + + +def network_modify(request, network_id, **kwargs): + LOG.debug("network_modify(): netid=%s, params=%s" % (network_id, kwargs)) + body = {'network': kwargs} + network = quantumclient(request).update_network(network_id, + body=body).get('network') + return Network(network) + + +def network_delete(request, network_id): + LOG.debug("network_delete(): netid=%s" % network_id) + quantumclient(request).delete_network(network_id) + + +def subnet_list(request, **params): + LOG.debug("subnet_list(): params=%s" % (params)) + subnets = quantumclient(request).list_subnets(**params).get('subnets') + return [Subnet(s) for s in subnets] + + +def subnet_get(request, subnet_id, **params): + LOG.debug("subnet_get(): subnetid=%s, params=%s" % (subnet_id, params)) + subnet = quantumclient(request).show_subnet(subnet_id, + **params).get('subnet') + return Subnet(subnet) + + +def subnet_create(request, network_id, cidr, ip_version, **kwargs): + """ + Create a subnet on a specified network. + :param request: request context + :param network_id: network id a subnet is created on + :param cidr: subnet IP address range + :param ip_version: IP version (4 or 6) + :param gateway_ip: (optional) IP address of gateway + :param tenant_id: (optional) tenant id of the subnet created + :param name: (optional) name of the subnet created + :returns: Subnet object + """ + LOG.debug("subnet_create(): netid=%s, cidr=%s, ipver=%d, kwargs=%s" + % (network_id, cidr, ip_version, kwargs)) + body = {'subnet': + {'network_id': network_id, + 'ip_version': ip_version, + 'cidr': cidr}} + body['subnet'].update(kwargs) + subnet = quantumclient(request).create_subnet(body=body).get('subnet') + return Subnet(subnet) + + +def subnet_modify(request, subnet_id, **kwargs): + LOG.debug("subnet_modify(): subnetid=%s, kwargs=%s" % (subnet_id, kwargs)) + body = {'subnet': kwargs} + subnet = quantumclient(request).update_subnet(subnet_id, + body=body).get('subnet') + return Subnet(subnet) + + +def subnet_delete(request, subnet_id): + LOG.debug("subnet_delete(): subnetid=%s" % subnet_id) + quantumclient(request).delete_subnet(subnet_id) + + +def port_list(request, **params): + LOG.debug("port_list(): params=%s" % (params)) + ports = quantumclient(request).list_ports(**params).get('ports') + return [Port(p) for p in ports] + + +def port_get(request, port_id, **params): + LOG.debug("port_get(): portid=%s, params=%s" % (port_id, params)) + port = quantumclient(request).show_port(port_id, **params).get('port') + return Port(port) + + +def port_create(request, network_id, **kwargs): + """ + Create a port on a specified network. + :param request: request context + :param network_id: network id a subnet is created on + :param device_id: (optional) device id attached to the port + :param tenant_id: (optional) tenant id of the port created + :param name: (optional) name of the port created + :returns: Port object + """ + LOG.debug("port_create(): netid=%s, kwargs=%s" % (network_id, kwargs)) + body = {'port': {'network_id': network_id}} + body['port'].update(kwargs) + port = quantumclient(request).create_port(body=body).get('port') + return Port(port) + + +def port_delete(request, port_id): + LOG.debug("port_delete(): portid=%s" % port_id) + quantumclient(request).delete_port(port_id) + + +def port_modify(request, port_id, **kwargs): + LOG.debug("port_modify(): portid=%s, kwargs=%s" % (port_id, kwargs)) + body = {'port': kwargs} + port = quantumclient(request).update_port(port_id, body=body).get('port') + return Port(port) diff --git a/horizon/dashboards/nova/dashboard.py b/horizon/dashboards/nova/dashboard.py index e950873c39..822f926210 100644 --- a/horizon/dashboards/nova/dashboard.py +++ b/horizon/dashboards/nova/dashboard.py @@ -26,7 +26,8 @@ class BasePanels(horizon.PanelGroup): 'instances', 'volumes', 'images_and_snapshots', - 'access_and_security') + 'access_and_security', + 'networks') class ObjectStorePanels(horizon.PanelGroup): diff --git a/horizon/dashboards/nova/instances/tests.py b/horizon/dashboards/nova/instances/tests.py index 38f24aa63b..d093d19d4a 100644 --- a/horizon/dashboards/nova/instances/tests.py +++ b/horizon/dashboards/nova/instances/tests.py @@ -589,6 +589,7 @@ class InstanceTests(test.TestCase): 'security_group_list', 'volume_snapshot_list', 'volume_list',), + api.quantum: ('network_list',), api.glance: ('image_list_detailed',)}) def test_launch_instance_get(self): quota_usages = self.quota_usages.first() @@ -604,6 +605,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(quota_usages) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -631,10 +634,12 @@ class InstanceTests(test.TestCase): self.assertQuerysetEqual(workflow.steps, ['', '', + '', '', '']) @test.create_stubs({api.glance: ('image_list_detailed',), + api.quantum: ('network_list',), api.nova: ('flavor_list', 'keypair_list', 'security_group_list', @@ -653,6 +658,7 @@ class InstanceTests(test.TestCase): device_name = u'vda' volume_choice = "%s:vol" % volume.id block_device_mapping = {device_name: u"%s::0" % volume_choice} + nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -666,6 +672,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) @@ -677,6 +685,7 @@ class InstanceTests(test.TestCase): customization_script, [sec_group.name], block_device_mapping, + nics=nics, instance_count=IsA(int)) self.mox.ReplayAll() @@ -693,6 +702,7 @@ class InstanceTests(test.TestCase): 'volume_type': 'volume_id', 'volume_id': volume_choice, 'device_name': device_name, + 'network': self.networks.first().id, 'count': 1} url = reverse('horizon:nova:instances:launch') res = self.client.post(url, form_data) @@ -702,6 +712,7 @@ class InstanceTests(test.TestCase): reverse('horizon:nova:instances:index')) @test.create_stubs({api.glance: ('image_list_detailed',), + api.quantum: ('network_list',), api.nova: ('flavor_list', 'keypair_list', 'security_group_list', @@ -727,6 +738,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.nova.keypair_list(IsA(http.HttpRequest)) \ @@ -762,6 +775,7 @@ class InstanceTests(test.TestCase): 'nova/instances/launch.html') @test.create_stubs({api.glance: ('image_list_detailed',), + api.quantum: ('network_list',), api.nova: ('tenant_quota_usages', 'flavor_list', 'keypair_list', @@ -779,6 +793,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(self.quota_usages.first()) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -799,6 +815,7 @@ class InstanceTests(test.TestCase): 'nova/instances/launch.html') @test.create_stubs({api.glance: ('image_list_detailed',), + api.quantum: ('network_list',), api.nova: ('flavor_list', 'keypair_list', 'security_group_list', @@ -812,6 +829,7 @@ class InstanceTests(test.TestCase): server = self.servers.first() sec_group = self.security_groups.first() customization_script = 'userData' + nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) @@ -825,6 +843,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list()) api.nova.server_create(IsA(http.HttpRequest), server.name, @@ -834,6 +854,7 @@ class InstanceTests(test.TestCase): customization_script, [sec_group.name], None, + nics=nics, instance_count=IsA(int)) \ .AndRaise(self.exceptions.keystone) @@ -849,6 +870,7 @@ class InstanceTests(test.TestCase): 'user_id': self.user.id, 'groups': sec_group.name, 'volume_type': '', + 'network': self.networks.first().id, 'count': 1} url = reverse('horizon:nova:instances:launch') res = self.client.post(url, form_data) @@ -856,6 +878,7 @@ class InstanceTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api.glance: ('image_list_detailed',), + api.quantum: ('network_list',), api.nova: ('flavor_list', 'keypair_list', 'security_group_list', @@ -885,6 +908,8 @@ class InstanceTests(test.TestCase): api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id}) \ .AndReturn([[], False]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) diff --git a/horizon/dashboards/nova/instances/workflows.py b/horizon/dashboards/nova/instances/workflows.py index c7155d6b6d..eb04e64d0c 100644 --- a/horizon/dashboards/nova/instances/workflows.py +++ b/horizon/dashboards/nova/instances/workflows.py @@ -18,6 +18,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from django.utils.text import normalize_newlines from django.utils.translation import ugettext as _ @@ -28,6 +30,9 @@ from horizon import workflows from horizon.openstack.common import jsonutils +LOG = logging.getLogger(__name__) + + class SelectProjectUserAction(workflows.Action): project_id = forms.ChoiceField(label=_("Project")) user_id = forms.ChoiceField(label=_("User")) @@ -400,6 +405,46 @@ class PostCreationStep(workflows.Step): contributes = ("customization_script",) +class SetNetworkAction(workflows.Action): + network = forms.MultipleChoiceField(label=_("Networks"), + required=True, + widget=forms.CheckboxSelectMultiple(), + help_text=_("Launch instance with" + "these networks")) + + class Meta: + name = _("Networking") + permissions = ('openstack.services.network',) + help_text = _("Select networks for your instance.") + + def populate_network_choices(self, request, context): + try: + networks = api.quantum.network_list(request) + for n in networks: + n.set_id_as_name_if_empty() + network_list = [(network.id, network.name) for network in networks] + except: + network_list = [] + exceptions.handle(request, + _('Unable to retrieve networks.')) + return network_list + + +class SetNetwork(workflows.Step): + action_class = SetNetworkAction + contributes = ("network_id",) + + def contribute(self, data, context): + if data: + networks = self.workflow.request.POST.getlist("network") + # If no networks are explicitly specified, network list + # contains an empty string, so remove it. + networks = [n for n in networks if n != ''] + if networks: + context['network_id'] = networks + return context + + class LaunchInstance(workflows.Workflow): slug = "launch_instance" name = _("Launch Instance") @@ -410,6 +455,7 @@ class LaunchInstance(workflows.Workflow): default_steps = (SelectProjectUser, SetInstanceDetails, SetAccessControls, + SetNetwork, VolumeOptions, PostCreationStep) @@ -437,6 +483,13 @@ class LaunchInstance(workflows.Workflow): else: dev_mapping = None + netids = context.get('network_id', None) + if netids: + nics = [{"net-id": netid, "v4-fixed-ip": ""} + for netid in netids] + else: + nics = None + try: api.nova.server_create(request, context['name'], @@ -446,6 +499,7 @@ class LaunchInstance(workflows.Workflow): normalize_newlines(custom_script), context['security_group_ids'], dev_mapping, + nics=nics, instance_count=int(context['count'])) return True except: diff --git a/horizon/dashboards/nova/networks/__init__.py b/horizon/dashboards/nova/networks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/nova/networks/forms.py b/horizon/dashboards/nova/networks/forms.py new file mode 100644 index 0000000000..0ca4797590 --- /dev/null +++ b/horizon/dashboards/nova/networks/forms.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import messages + + +LOG = logging.getLogger(__name__) + + +class UpdateNetwork(forms.SelfHandlingForm): + name = forms.CharField(label=_("Name"), required=False) + tenant_id = forms.CharField(widget=forms.HiddenInput) + network_id = forms.CharField(label=_("ID"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + failure_url = 'horizon:nova:networks:index' + + def handle(self, request, data): + try: + network = api.quantum.network_modify(request, data['network_id'], + name=data['name']) + msg = _('Network %s was successfully updated.') % data['name'] + LOG.debug(msg) + messages.success(request, msg) + return network + except: + msg = _('Failed to update network %s') % data['name'] + LOG.info(msg) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) diff --git a/horizon/dashboards/nova/networks/panel.py b/horizon/dashboards/nova/networks/panel.py new file mode 100644 index 0000000000..e4270bec4a --- /dev/null +++ b/horizon/dashboards/nova/networks/panel.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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_lazy as _ + +import horizon +from horizon.dashboards.nova import dashboard + + +class Networks(horizon.Panel): + name = _("Networks") + slug = 'networks' + permissions = ('openstack.services.network',) + +dashboard.Nova.register(Networks) diff --git a/horizon/dashboards/nova/networks/ports/__init__.py b/horizon/dashboards/nova/networks/ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/nova/networks/ports/tables.py b/horizon/dashboards/nova/networks/ports/tables.py new file mode 100644 index 0000000000..0483d15ea5 --- /dev/null +++ b/horizon/dashboards/nova/networks/ports/tables.py @@ -0,0 +1,53 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django import template +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +def get_fixed_ips(port): + template_name = 'nova/networks/ports/_port_ips.html' + context = {"ips": port.fixed_ips} + return template.loader.render_to_string(template_name, context) + + +def get_attached(port): + return _('Attached') if port['device_id'] else _('Detached') + + +class PortsTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:nova:networks:ports:detail") + fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs")) + attached = tables.Column(get_attached, verbose_name=_("Device Attached")) + status = tables.Column("status", verbose_name=_("Status")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + def get_object_display(self, port): + return port.id + + class Meta: + name = "ports" + verbose_name = _("Ports") diff --git a/horizon/dashboards/nova/networks/ports/tabs.py b/horizon/dashboards/nova/networks/ports/tabs.py new file mode 100644 index 0000000000..f4ebf2f4cb --- /dev/null +++ b/horizon/dashboards/nova/networks/ports/tabs.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tabs + +import logging +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "nova/networks/ports/_detail_overview.html" + + def get_context_data(self, request): + port_id = self.tab_group.kwargs['port_id'] + try: + port = api.quantum.port_get(self.request, port_id) + except: + redirect = reverse('horizon:nova:networks:index') + msg = _('Unable to retrieve port details.') + exceptions.handle(request, msg, redirect=redirect) + return {'port': port} + + +class PortDetailTabs(tabs.TabGroup): + slug = "port_details" + tabs = (OverviewTab,) diff --git a/horizon/dashboards/nova/networks/ports/urls.py b/horizon/dashboards/nova/networks/ports/urls.py new file mode 100644 index 0000000000..67fab54795 --- /dev/null +++ b/horizon/dashboards/nova/networks/ports/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 + +from .views import DetailView + +PORTS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.nova.networks.ports.views', + url(PORTS % 'detail', DetailView.as_view(), name='detail')) diff --git a/horizon/dashboards/nova/networks/ports/views.py b/horizon/dashboards/nova/networks/ports/views.py new file mode 100644 index 0000000000..17fc435440 --- /dev/null +++ b/horizon/dashboards/nova/networks/ports/views.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 horizon import tabs +from .tabs import PortDetailTabs + + +LOG = logging.getLogger(__name__) + + +class DetailView(tabs.TabView): + tab_group_class = PortDetailTabs + template_name = 'nova/networks/ports/detail.html' diff --git a/horizon/dashboards/nova/networks/subnets/__init__.py b/horizon/dashboards/nova/networks/subnets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/nova/networks/subnets/forms.py b/horizon/dashboards/nova/networks/subnets/forms.py new file mode 100644 index 0000000000..111db3664d --- /dev/null +++ b/horizon/dashboards/nova/networks/subnets/forms.py @@ -0,0 +1,138 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 netaddr + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import forms +from horizon import messages +from horizon import exceptions +from horizon.utils import fields + + +LOG = logging.getLogger(__name__) + + +class CreateSubnet(forms.SelfHandlingForm): + network_name = forms.CharField(label=_("Network Name"), + required=False, + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + network_id = forms.CharField(label=_("Network ID"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + cidr = fields.IPField(label=_("Network Address"), + required=True, + initial="", + help_text=_("Network address in CIDR format " + "(e.g. 192.168.0.0/24)"), + version=fields.IPv4 | fields.IPv6, + mask=True) + ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], + label=_("IP Version")) + gateway_ip = fields.IPField(label=_("Gateway IP"), + required=False, + initial="", + help_text=_("IP address of Gateway " + "(e.g. 192.168.0.1)"), + version=fields.IPv4 | fields.IPv6, + mask=False) + failure_url = 'horizon:nova:networks:detail' + + def clean(self): + cleaned_data = super(CreateSubnet, self).clean() + cidr = cleaned_data.get('cidr') + ip_version = int(cleaned_data.get('ip_version')) + gateway_ip = cleaned_data.get('gateway_ip') + if cidr: + if netaddr.IPNetwork(cidr).version is not ip_version: + msg = _('Network Address and IP version are inconsistent.') + raise forms.ValidationError(msg) + if gateway_ip: + if netaddr.IPAddress(gateway_ip).version is not ip_version: + msg = _('Gateway IP and IP version are inconsistent.') + raise forms.ValidationError(msg) + return cleaned_data + + def handle(self, request, data): + try: + LOG.debug('params = %s' % data) + data['ip_version'] = int(data['ip_version']) + if not data['gateway_ip']: + del data['gateway_ip'] + subnet = api.quantum.subnet_create(request, **data) + msg = _('Subnet %s was successfully created.') % data['cidr'] + LOG.debug(msg) + messages.success(request, msg) + return subnet + except Exception: + msg = _('Failed to create subnet %s') % data['cidr'] + LOG.info(msg) + redirect = reverse(self.failure_url, args=[data['network_id']]) + exceptions.handle(request, msg, redirect=redirect) + + +class UpdateSubnet(forms.SelfHandlingForm): + network_id = forms.CharField(widget=forms.HiddenInput()) + subnet_id = forms.CharField(widget=forms.HiddenInput()) + cidr = forms.CharField(widget=forms.HiddenInput()) + ip_version = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + gateway_ip = fields.IPField(label=_("Gateway IP"), + required=True, + initial="", + help_text=_("IP address of Gateway " + "(e.g. 192.168.0.1)"), + version=fields.IPv4 | fields.IPv6, + mask=False) + failure_url = 'horizon:nova:networks:detail' + + def clean(self): + cleaned_data = super(UpdateSubnet, self).clean() + ip_version = int(cleaned_data.get('ip_version')) + gateway_ip = cleaned_data.get('gateway_ip') + if gateway_ip: + if netaddr.IPAddress(gateway_ip).version is not ip_version: + msg = _('Gateway IP and IP version are inconsistent.') + raise forms.ValidationError(msg) + return cleaned_data + + def handle(self, request, data): + try: + LOG.debug('params = %s' % data) + params = {'name': data['name']} + params['gateway_ip'] = data['gateway_ip'] + subnet = api.quantum.subnet_modify(request, data['subnet_id'], + name=data['name'], + gateway_ip=data['gateway_ip']) + msg = _('Subnet %s was successfully updated.') % data['cidr'] + LOG.debug(msg) + messages.success(request, msg) + return subnet + except Exception: + msg = _('Failed to update subnet %s') % data['cidr'] + LOG.info(msg) + redirect = reverse(self.failure_url, args=[data['network_id']]) + exceptions.handle(request, msg, redirect=redirect) diff --git a/horizon/dashboards/nova/networks/subnets/tables.py b/horizon/dashboards/nova/networks/subnets/tables.py new file mode 100644 index 0000000000..a32e5eef31 --- /dev/null +++ b/horizon/dashboards/nova/networks/subnets/tables.py @@ -0,0 +1,79 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class DeleteSubnet(tables.DeleteAction): + data_type_singular = _("Subnet") + data_type_plural = _("Subnets") + + def delete(self, request, obj_id): + try: + api.quantum.subnet_delete(request, obj_id) + except: + msg = _('Failed to delete subnet %s') % obj_id + LOG.info(msg) + network_id = self.table.kwargs['network_id'] + redirect = reverse('horizon:nova:networks:detail', + args=[network_id]) + exceptions.handle(request, msg, redirect=redirect) + + +class CreateSubnet(tables.LinkAction): + name = "create" + verbose_name = _("Create Subnet") + url = "horizon:nova:networks:addsubnet" + classes = ("ajax-modal", "btn-create") + + def get_link_url(self, datum=None): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id,)) + + +class UpdateSubnet(tables.LinkAction): + name = "update" + verbose_name = _("Edit Subnet") + url = "horizon:nova:networks:editsubnet" + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, subnet): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id, subnet.id)) + + +class SubnetsTable(tables.DataTable): + name = tables.Column("name", verbose_name=_("Name"), + link='horizon:nova:networks:subnets:detail') + cidr = tables.Column("cidr", verbose_name=_("Network Address")) + ip_version = tables.Column("ipver_str", verbose_name=_("IP Version")) + gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP")) + + class Meta: + name = "subnets" + verbose_name = _("Subnets") + table_actions = (CreateSubnet, DeleteSubnet) + row_actions = (UpdateSubnet, DeleteSubnet) diff --git a/horizon/dashboards/nova/networks/subnets/tabs.py b/horizon/dashboards/nova/networks/subnets/tabs.py new file mode 100644 index 0000000000..c832fba6ba --- /dev/null +++ b/horizon/dashboards/nova/networks/subnets/tabs.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tabs + + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "nova/networks/subnets/_detail_overview.html" + + def get_context_data(self, request): + subnet_id = self.tab_group.kwargs['subnet_id'] + try: + subnet = api.quantum.subnet_get(self.request, subnet_id) + except: + redirect = reverse('horizon:nova:networks:index') + msg = _('Unable to retrieve subnet details.') + exceptions.handle(request, msg, redirect=redirect) + return {'subnet': subnet} + + +class SubnetDetailTabs(tabs.TabGroup): + slug = "subnet_details" + tabs = (OverviewTab,) diff --git a/horizon/dashboards/nova/networks/subnets/urls.py b/horizon/dashboards/nova/networks/subnets/urls.py new file mode 100644 index 0000000000..9e4523f2aa --- /dev/null +++ b/horizon/dashboards/nova/networks/subnets/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 + +from .views import DetailView + +SUBNETS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.nova.networks.subnets.views', + url(SUBNETS % 'detail', DetailView.as_view(), name='detail')) diff --git a/horizon/dashboards/nova/networks/subnets/views.py b/horizon/dashboards/nova/networks/subnets/views.py new file mode 100644 index 0000000000..d773beca8a --- /dev/null +++ b/horizon/dashboards/nova/networks/subnets/views.py @@ -0,0 +1,109 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 Quantum Subnets. +""" +import logging + +from django.core.urlresolvers import reverse_lazy, reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import exceptions +from horizon import api +from horizon import tabs +from .forms import CreateSubnet, UpdateSubnet +from .tabs import SubnetDetailTabs + +LOG = logging.getLogger(__name__) + + +class CreateView(forms.ModalFormView): + form_class = CreateSubnet + template_name = 'nova/networks/subnets/create.html' + success_url = 'horizon:nova:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def get_object(self): + if not hasattr(self, "_object"): + try: + network_id = self.kwargs["network_id"] + self._object = api.quantum.network_get(self.request, + network_id) + except: + redirect = reverse('horizon:nova:networks:index') + msg = _("Unable to retrieve network.") + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(CreateView, self).get_context_data(**kwargs) + context['network'] = self.get_object() + return context + + def get_initial(self): + network = self.get_object() + return {"network_id": self.kwargs['network_id'], + "network_name": network.name} + + +class UpdateView(forms.ModalFormView): + form_class = UpdateSubnet + template_name = 'nova/networks/subnets/update.html' + context_object_name = 'subnet' + success_url = reverse_lazy('horizon:nova:networks:detail') + + def get_success_url(self): + return reverse('horizon:nova:networks:detail', + args=(self.kwargs['network_id'],)) + + def _get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + subnet_id = self.kwargs['subnet_id'] + try: + self._object = api.quantum.subnet_get(self.request, subnet_id) + except: + redirect = reverse("horizon:nova:networks:index") + msg = _('Unable to retrieve subnet details') + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + subnet = self._get_object() + context['subnet_id'] = subnet.id + context['network_id'] = subnet.network_id + context['cidr'] = subnet.cidr + context['ip_version'] = subnet.ipver_str + return context + + def get_initial(self): + subnet = self._get_object() + return {'network_id': self.kwargs['network_id'], + 'subnet_id': subnet['id'], + 'cidr': subnet['cidr'], + 'ip_version': subnet['ip_version'], + 'name': subnet['name'], + 'gateway_ip': subnet['gateway_ip']} + + +class DetailView(tabs.TabView): + tab_group_class = SubnetDetailTabs + template_name = 'nova/networks/subnets/detail.html' diff --git a/horizon/dashboards/nova/networks/tables.py b/horizon/dashboards/nova/networks/tables.py new file mode 100644 index 0000000000..d94ca07666 --- /dev/null +++ b/horizon/dashboards/nova/networks/tables.py @@ -0,0 +1,94 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import logging + +from django import template +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class DeleteNetwork(tables.DeleteAction): + data_type_singular = _("Network") + data_type_plural = _("Networks") + + def delete(self, request, network_id): + try: + # Retrieve existing subnets belonging to the network. + subnets = api.quantum.subnet_list(request, network_id=network_id) + LOG.debug('Network %s has subnets: %s' % + (network_id, [s.id for s in subnets])) + for s in subnets: + api.quantum.subnet_delete(request, s.id) + LOG.debug('Deleted subnet %s' % s.id) + + api.quantum.network_delete(request, network_id) + LOG.debug('Deleted network %s successfully' % network_id) + except: + msg = _('Failed to delete network %s') % network_id + LOG.info(msg) + redirect = reverse("horizon:nova:networks:index") + exceptions.handle(request, msg, redirect=redirect) + + +class CreateNetwork(tables.LinkAction): + name = "create" + verbose_name = _("Create Network") + url = "horizon:nova:networks:create" + classes = ("ajax-modal", "btn-create") + + +class EditNetwork(tables.LinkAction): + name = "update" + verbose_name = _("Edit Network") + url = "horizon:nova:networks:update" + classes = ("ajax-modal", "btn-edit") + + +class CreateSubnet(tables.LinkAction): + name = "subnet" + verbose_name = _("Add Subnet") + url = "horizon:nova:networks:addsubnet" + classes = ("ajax-modal", "btn-create") + + +def get_subnets(network): + template_name = 'nova/networks/_network_ips.html' + context = {"subnets": network.subnets} + return template.loader.render_to_string(template_name, context) + + +class NetworksTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link='horizon:nova:networks:detail') + subnets = tables.Column(get_subnets, + verbose_name=_("Subnets Associated"),) + status = tables.Column("status", verbose_name=_("Status")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + class Meta: + name = "networks" + verbose_name = _("Networks") + table_actions = (CreateNetwork, DeleteNetwork) + row_actions = (EditNetwork, CreateSubnet, DeleteNetwork) diff --git a/horizon/dashboards/nova/networks/templates/networks/_create.html b/horizon/dashboards/nova/networks/templates/networks/_create.html new file mode 100644 index 0000000000..664a6b99ab --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/_create.html @@ -0,0 +1,24 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_network_form{% endblock %} +{% block form_action %}{% url horizon:nova:networks:create %}{% endblock %} + +{% block modal-header %}{% trans "Create Network" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "Select a name for your network."%}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html new file mode 100644 index 0000000000..a1d3d7ffc0 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html @@ -0,0 +1,18 @@ +{% load i18n sizeformat %} + +

{% trans "Network Overview" %}

+ +
+
+
{% trans "Name" %}
+
{{ network.name|default:"None" }}
+
{% trans "ID" %}
+
{{ network.id|default:"None" }}
+
{% trans "Project ID" %}
+
{{ network.tenant_id|default:"-" }}
+
{% trans "Status" %}
+
{{ network.status|default:"Unknown" }}
+
{% trans "Admin State" %}
+
{{ network.admin_state|default:"Unknown" }}
+
+
diff --git a/horizon/dashboards/nova/networks/templates/networks/_network_ips.html b/horizon/dashboards/nova/networks/templates/networks/_network_ips.html new file mode 100644 index 0000000000..a80e74795b --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/_network_ips.html @@ -0,0 +1,10 @@ +{% for subnet in subnets %} +
    +
  • + {% if subnet.name|length > 0 %} + {{ subnet.name }} + {% endif %} + {{ subnet.cidr }} +
  • +
+{% endfor %} diff --git a/horizon/dashboards/nova/networks/templates/networks/_update.html b/horizon/dashboards/nova/networks/templates/networks/_update.html new file mode 100644 index 0000000000..50413cfe04 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/_update.html @@ -0,0 +1,24 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}update_network_form{% endblock %} +{% block form_action %}{% url horizon:nova:networks:update network_id %}{% endblock %} + +{% block modal-header %}{% trans "Edit Network" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "You may update the editable properties of your network here." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/create.html b/horizon/dashboards/nova/networks/templates/networks/create.html new file mode 100644 index 0000000000..5a9d7da31d --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Network" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Network") %} +{% endblock page_header %} + +{% block main %} + {% include "horizon/common/_workflow.html" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/detail.html b/horizon/dashboards/nova/networks/templates/networks/detail.html new file mode 100644 index 0000000000..3a8ac2d87d --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/detail.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Network Detail"%}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Network Detail: ")|add:network.name %} +{% endblock page_header %} + +{% block main %} + {% include "nova/networks/_detail_overview.html" %} +
+
+ {{ subnets_table.render }} +
+
+ {{ ports_table.render }} +
+{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/index.html b/horizon/dashboards/nova/networks/templates/networks/index.html new file mode 100644 index 0000000000..d458220ab8 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Networks" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Networks") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html new file mode 100644 index 0000000000..401a5db695 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html @@ -0,0 +1,41 @@ +{% load i18n sizeformat %} + +

{% trans "Port Overview" %}

+ +
+

{% trans "Port" %}

+
+
+
{% trans "Name" %}
+
{{ port.name|default:"None" }}
+
{% trans "ID" %}
+
{{ port.id|default:"None" }}
+
{% trans "Network ID" %}
+
{{ port.network_id|default:"None" }}
+
{% trans "Project ID" %}
+
{{ port.tenant_id|default:"-" }}
+
{% trans "Fixed IP" %}
+
+ {% if port.fixed_ips.items|length > 1 %} + {% for ip in port.fixed_ips %} + {% trans "IP address:" %} {{ ip.ip_address }}, + {% trans "Subnet ID" %} {{ ip.subnet_id }}
+ {% endfor %} + {% else %} + "None" + {% endif %} +
+
{% trans "Mac Address" %}
+
{{ port.mac_address|default:"None" }}
+
{% trans "Status" %}
+
{{ port.status|default:"None" }}
+
{% trans "Admin State" %}
+
{{ port.admin_state|default:"None" }}
+
{% trans "Device ID" %}
+ {% if port.device_id|length > 1 %} +
{{ port.device_id }}
+ {% else %} +
No attached device
+ {% endif %} +
+
diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html b/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html new file mode 100644 index 0000000000..bfd5ea9f1f --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html @@ -0,0 +1,7 @@ +{% for ip in ips %} +
    +
  • + {{ ip.ip_address }} +
  • +
+{% endfor %} diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/detail.html b/horizon/dashboards/nova/networks/templates/networks/ports/detail.html new file mode 100644 index 0000000000..634c6d67e0 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/ports/detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Port Detail"%}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Port Detail") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html new file mode 100644 index 0000000000..942ece067e --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_subnet_form{% endblock %} +{% block form_action %}{% url horizon:nova:networks:addsubnet network.id %} +{% endblock %} + +{% block modal-header %}{% trans "Create Subnet" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "You can create a subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html new file mode 100644 index 0000000000..4c09fde091 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html @@ -0,0 +1,29 @@ +{% load i18n sizeformat %} + +

{% trans "Subnet Overview" %}

+ +
+

{% trans "Subnet" %}

+
+
+
{% trans "Name" %}
+
{{ subnet.name|default:"None" }}
+
{% trans "ID" %}
+
{{ subnet.id|default:"None" }}
+
{% trans "Network ID" %}
+
{{ subnet.network_id|default:"None" }}
+
{% trans "CIDR" %}
+
{{ subnet.cidr|default:"None" }}
+
{% trans "IP version" %}
+
{{ subnet.ipver_str|default:"-" }}
+
{% trans "Gateway IP" %}
+
{{ subnet.gateway_ip|default:"-" }}
+
{% trans "IP allocation pool" %}
+
+ {% for pool in subnet.allocation_pools %} + {% trans "Start" %} {{ pool.start }} + {% trans " - End" %} {{ pool.end }}
+ {% endfor %} +
+
+
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html new file mode 100644 index 0000000000..4093b06c7d --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html @@ -0,0 +1,33 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}update_subnet_form{% endblock %} +{% block form_action %}{% url horizon:nova:networks:editsubnet network_id subnet_id %}{% endblock %} + +{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %} + +{% block modal-body %} +
+
+
{% trans "ID" %}
+
{{ subnet_id }}
+
{% trans "Network Address" %}
+
{{ cidr }}
+
{% trans "IP version" %}
+
{{ ip_version }}
+
+
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "You may update the editable properties of your subnet here." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/create.html b/horizon/dashboards/nova/networks/templates/networks/subnets/create.html new file mode 100644 index 0000000000..01e052c2ed --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Subnet" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Subnet") %} +{% endblock page_header %} + +{% block main %} + {% include "nova/networks/subnets/_create.html" %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html b/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html new file mode 100644 index 0000000000..c4e35bd071 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Subnet Detail"%}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Subnet Detail") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/index.html b/horizon/dashboards/nova/networks/templates/networks/subnets/index.html new file mode 100644 index 0000000000..833399a228 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Network" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Network") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/update.html b/horizon/dashboards/nova/networks/templates/networks/subnets/update.html new file mode 100644 index 0000000000..d5b8137288 --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/subnets/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Subnet" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Subnet") %} +{% endblock page_header %} + +{% block main %} + {% include 'nova/networks/subnets/_update.html' %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/templates/networks/update.html b/horizon/dashboards/nova/networks/templates/networks/update.html new file mode 100644 index 0000000000..599de61acd --- /dev/null +++ b/horizon/dashboards/nova/networks/templates/networks/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Network" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Network") %} +{% endblock page_header %} + +{% block main %} + {% include 'nova/networks/_update.html' %} +{% endblock %} diff --git a/horizon/dashboards/nova/networks/tests.py b/horizon/dashboards/nova/networks/tests.py new file mode 100644 index 0000000000..da15e9b247 --- /dev/null +++ b/horizon/dashboards/nova/networks/tests.py @@ -0,0 +1,753 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.http import urlencode +from django.utils.html import escape +from django.utils.datastructures import SortedDict +from mox import IsA, IgnoreArg +from copy import deepcopy + +from horizon import api +from horizon import test + +from .workflows import CreateNetwork + + +INDEX_URL = reverse('horizon:nova:networks:index') + + +class NetworkTests(test.TestCase): + @test.create_stubs({api.quantum: ('network_list',)}) + def test_index(self): + api.quantum.network_list(IsA(http.HttpRequest), + tenant_id=self.tenant.id) \ + .AndReturn(self.networks.list()) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'nova/networks/index.html') + networks = res.context['networks_table'].data + self.assertItemsEqual(networks, self.networks.list()) + + @test.create_stubs({api.quantum: ('network_list',)}) + def test_index_network_list_exception(self): + api.quantum.network_list(IsA(http.HttpRequest), + tenant_id=self.tenant.id) \ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'nova/networks/index.html') + self.assertEqual(len(res.context['networks_table'].data), 0) + self.assertMessageCount(res, error=1) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertItemsEqual(subnets, [self.subnets.first()]) + self.assertItemsEqual(ports, [self.ports.first()]) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_network_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:networks:detail', args=[network_id]) + res = self.client.get(url) + + redir_url = INDEX_URL + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_subnet_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id).\ + AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\ + AndRaise(self.exceptions.quantum) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\ + AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertEqual(len(subnets), 0) + self.assertItemsEqual(ports, [self.ports.first()]) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_port_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id).\ + AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\ + AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\ + AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertItemsEqual(subnets, [self.subnets.first()]) + self.assertEqual(len(ports), 0) + + def test_network_create_get(self): + # no api methods are called. + self.mox.ReplayAll() + + url = reverse('horizon:nova:networks:create') + res = self.client.get(url) + + workflow = res.context['workflow'] + self.assertTemplateUsed(res, 'nova/networks/create.html') + self.assertEqual(workflow.name, CreateNetwork.name) + expected_objs = ['', + ''] + self.assertQuerysetEqual(workflow.steps, expected_objs) + + @test.create_stubs({api.quantum: ('network_create',)}) + def test_network_create_post(self): + network = self.networks.first() + api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + .AndReturn(network) + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': False, + 'subnet_name': '', + 'cidr': '', + 'ip_version': 4, + 'gateway_ip': ''} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_create', + 'subnet_create',)}) + def test_network_create_post_with_subnet(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + .AndReturn(network) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_create',)}) + def test_network_create_post_network_exception(self): + network = self.networks.first() + api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': False, + 'subnet_name': '', + 'cidr': '', + 'ip_version': 4, + 'gateway_ip': ''} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_create',)}) + def test_network_create_post_with_subnet_network_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_create', + 'subnet_create',)}) + def test_network_create_post_with_subnet_subnet_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\ + .AndReturn(network) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + def test_network_create_post_with_subnet_nocidr(self): + network = self.networks.first() + subnet = self.subnets.first() + self.mox.ReplayAll() + + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': '', + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertContains(res, escape('Specify "Network Address" or ' + 'clear "Create Subnet" checkbox.')) + + def test_network_create_post_with_subnet_cidr_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + self.mox.ReplayAll() + + # dummy IPv6 address + cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + expected_msg = 'Network Address and IP version are inconsistent.' + self.assertContains(res, expected_msg) + + def test_network_create_post_with_subnet_gw_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + self.mox.ReplayAll() + + # dummy IPv6 address + gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' + form_data = {'net_name': network.name, + 'with_subnet': True, + 'subnet_name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': gateway_ip} + url = reverse('horizon:nova:networks:create') + res = self.client.post(url, form_data) + + self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_network_update_get(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:networks:update', args=[network.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'nova/networks/update.html') + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_network_update_get_exception(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:networks:update', args=[network.id]) + res = self.client.get(url) + + redir_url = INDEX_URL + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_modify', + 'network_get',)}) + def test_network_update_post(self): + network = self.networks.first() + api.quantum.network_modify(IsA(http.HttpRequest), network.id, + name=network.name)\ + .AndReturn(network) + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + self.mox.ReplayAll() + + formData = {'network_id': network.id, + 'name': network.name, + 'tenant_id': network.tenant_id} + url = reverse('horizon:nova:networks:update', args=[network.id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_modify', + 'network_get',)}) + def test_network_update_post_exception(self): + network = self.networks.first() + api.quantum.network_modify(IsA(http.HttpRequest), network.id, + name=network.name)\ + .AndRaise(self.exceptions.quantum) + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + self.mox.ReplayAll() + + form_data = {'network_id': network.id, + 'name': network.name, + 'tenant_id': network.tenant_id} + url = reverse('horizon:nova:networks:update', args=[network.id]) + res = self.client.post(url, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list', + 'subnet_list', + 'network_delete')}) + def test_delete_network_no_subnet(self): + network = self.networks.first() + api.quantum.network_list(IsA(http.HttpRequest), + tenant_id=network.tenant_id)\ + .AndReturn([network]) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\ + .AndReturn([]) + api.quantum.network_delete(IsA(http.HttpRequest), network.id) + + self.mox.ReplayAll() + + form_data = {'action': 'networks__delete__%s' % network.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list', + 'subnet_list', + 'network_delete', + 'subnet_delete')}) + def test_delete_network_with_subnet(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_list(IsA(http.HttpRequest), + tenant_id=network.tenant_id)\ + .AndReturn([network]) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\ + .AndReturn([subnet]) + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id) + api.quantum.network_delete(IsA(http.HttpRequest), network.id) + + self.mox.ReplayAll() + + form_data = {'action': 'networks__delete__%s' % network.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list', + 'subnet_list', + 'network_delete', + 'subnet_delete')}) + def test_delete_network_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_list(IsA(http.HttpRequest), + tenant_id=network.tenant_id)\ + .AndReturn([network]) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\ + .AndReturn([subnet]) + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id) + api.quantum.network_delete(IsA(http.HttpRequest), network.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + form_data = {'action': 'networks__delete__%s' % network.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('subnet_get',)}) + def test_subnet_detail(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(self.subnets.first()) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:subnets:detail', + args=[subnet.id])) + + self.assertTemplateUsed(res, 'nova/networks/subnets/detail.html') + self.assertEqual(res.context['subnet'].id, subnet.id) + + @test.create_stubs({api.quantum: ('subnet_get',)}) + def test_subnet_detail_exception(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:subnets:detail', + args=[subnet.id])) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_get(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + url = reverse('horizon:nova:networks:addsubnet', + args=[network.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'nova/networks/subnets/create.html') + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + network_name=network.name, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:nova:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_network_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_subnet_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + network_name=network.name, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:nova:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_cidr_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + # dummy IPv6 address + cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + expected_msg = 'Network Address and IP version are inconsistent.' + self.assertContains(res, expected_msg) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_gw_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + # dummy IPv6 address + gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': gateway_ip} + url = reverse('horizon:nova:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, + name=subnet.name, + gateway_ip=subnet.gateway_ip)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + formData = {'network_id': subnet.network_id, + 'subnet_id': subnet.id, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:nova:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, formData) + + redir_url = reverse('horizon:nova:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_gw_inconsistent(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # dummy IPv6 address + gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' + formData = {'network_id': subnet.network_id, + 'subnet_id': subnet.id, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': gateway_ip} + url = reverse('horizon:nova:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, formData) + + self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + + @test.create_stubs({api.quantum: ('subnet_delete', + 'subnet_list', + 'port_list',)}) + def test_subnet_delete(self): + subnet = self.subnets.first() + network_id = subnet.network_id + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'subnets__delete__%s' % subnet.id} + url = reverse('horizon:nova:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.quantum: ('subnet_delete', + 'subnet_list', + 'port_list',)}) + def test_subnet_delete_exception(self): + subnet = self.subnets.first() + network_id = subnet.network_id + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'subnets__delete__%s' % subnet.id} + url = reverse('horizon:nova:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_detail(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(self.ports.first()) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:ports:detail', + args=[port.id])) + + self.assertTemplateUsed(res, 'nova/networks/ports/detail.html') + self.assertEqual(res.context['port'].id, port.id) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_detail_exception(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:nova:networks:ports:detail', + args=[port.id])) + + self.assertRedirectsNoFollow(res, INDEX_URL) diff --git a/horizon/dashboards/nova/networks/urls.py b/horizon/dashboards/nova/networks/urls.py new file mode 100644 index 0000000000..f5fb60e9c3 --- /dev/null +++ b/horizon/dashboards/nova/networks/urls.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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, include + +from .views import IndexView, CreateView, DetailView, UpdateView +from .subnets.views import CreateView as AddSubnetView +from .subnets.views import UpdateView as EditSubnetView +from .subnets import urls as subnet_urls +from .ports import urls as port_urls + +NETWORKS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create$', CreateView.as_view(), name='create'), + url(NETWORKS % 'detail', DetailView.as_view(), name='detail'), + url(NETWORKS % 'update', UpdateView.as_view(), name='update'), + url(NETWORKS % 'subnets/create', AddSubnetView.as_view(), + name='addsubnet'), + url(r'^(?P[^/]+)/subnets/(?P[^/]+)/update$', + EditSubnetView.as_view(), name='editsubnet'), + url(r'^subnets/', include(subnet_urls, namespace='subnets')), + url(r'^ports/', include(port_urls, namespace='ports'))) diff --git a/horizon/dashboards/nova/networks/views.py b/horizon/dashboards/nova/networks/views.py new file mode 100644 index 0000000000..a6b02a94b8 --- /dev/null +++ b/horizon/dashboards/nova/networks/views.py @@ -0,0 +1,146 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 Quantum Networks. +""" +import logging + +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon import workflows + +from .tables import NetworksTable +from .subnets.tables import SubnetsTable +from .ports.tables import PortsTable +from .forms import UpdateNetwork +from .workflows import CreateNetwork + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = NetworksTable + template_name = 'nova/networks/index.html' + + def get_data(self): + try: + # If a user has admin role, network list returned by Quantum API + # contains networks that does not belong to that tenant. + # So we need to specify tenant_id when calling network_list(). + tenant_id = self.request.user.tenant_id + networks = api.quantum.network_list(self.request, + tenant_id=tenant_id) + except: + networks = [] + msg = _('Network list can not be retrieved.') + exceptions.handle(self.request, msg) + for n in networks: + n.set_id_as_name_if_empty() + return networks + + +class CreateView(workflows.WorkflowView): + workflow_class = CreateNetwork + template_name = 'nova/networks/create.html' + + def get_initial(self): + pass + + +class UpdateView(forms.ModalFormView): + form_class = UpdateNetwork + template_name = 'nova/networks/update.html' + context_object_name = 'network' + success_url = reverse_lazy("horizon:nova:networks:index") + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + context["network_id"] = self.kwargs['network_id'] + return context + + def _get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + network_id = self.kwargs['network_id'] + try: + self._object = api.quantum.network_get(self.request, + network_id) + except: + redirect = self.success_url + msg = _('Unable to retrieve network details.') + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_initial(self): + network = self._get_object() + return {'network_id': network['id'], + 'tenant_id': network['tenant_id'], + 'name': network['name']} + + +class DetailView(tables.MultiTableView): + table_classes = (SubnetsTable, PortsTable) + template_name = 'nova/networks/detail.html' + failure_url = reverse_lazy('horizon:nova:networks:index') + + def get_subnets_data(self): + try: + network_id = self.kwargs['network_id'] + subnets = api.quantum.subnet_list(self.request, + network_id=network_id) + except: + subnets = [] + msg = _('Subnet list can not be retrieved.') + exceptions.handle(self.request, msg) + for s in subnets: + s.set_id_as_name_if_empty() + return subnets + + def get_ports_data(self): + try: + network_id = self.kwargs['network_id'] + ports = api.quantum.port_list(self.request, network_id=network_id) + except: + ports = [] + msg = _('Port list can not be retrieved.') + exceptions.handle(self.request, msg) + for p in ports: + p.set_id_as_name_if_empty() + return ports + + def _get_data(self): + if not hasattr(self, "_network"): + try: + network_id = self.kwargs['network_id'] + network = api.quantum.network_get(self.request, network_id) + network.set_id_as_name_if_empty(length=0) + except: + msg = _('Unable to retrieve details for network "%s".') \ + % (network_id) + exceptions.handle(self.request, msg, redirect=self.failure_url) + self._network = network + return self._network + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + context["network"] = self._get_data() + return context diff --git a/horizon/dashboards/nova/networks/workflows.py b/horizon/dashboards/nova/networks/workflows.py new file mode 100644 index 0000000000..1837694ba5 --- /dev/null +++ b/horizon/dashboards/nova/networks/workflows.py @@ -0,0 +1,162 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 netaddr + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import workflows +from horizon.utils import fields + + +LOG = logging.getLogger(__name__) + + +class CreateNetworkInfoAction(workflows.Action): + net_name = forms.CharField(max_length=255, + label=_("Network Name (optional)"), + required=False) + + class Meta: + name = ("Network") + help_text = _("From here you can create a new network.\n" + "In addition a subnet associated with the network " + "can be created in the next panel.") + + +class CreateNetworkInfo(workflows.Step): + action_class = CreateNetworkInfoAction + contributes = ("net_name",) + + +class CreateSubnetInfoAction(workflows.Action): + with_subnet = forms.BooleanField(label=_("Create Subnet"), + initial=True, required=False) + subnet_name = forms.CharField(max_length=255, + label=_("Subnet Name (optional)"), + required=False) + cidr = fields.IPField(label=_("Network Address"), + required=False, + initial="", + help_text=_("Network address in CIDR format " + "(e.g. 192.168.0.0/24)"), + version=fields.IPv4 | fields.IPv6, + mask=True) + ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')], + label=_("IP Version")) + gateway_ip = fields.IPField(label=_("Gateway IP (optional)"), + required=False, + initial="", + help_text=_("IP address of Gateway " + "(e.g. 192.168.0.1)"), + version=fields.IPv4 | fields.IPv6, + mask=False) + + class Meta: + name = ("Subnet") + help_text = _("You can create a subnet associated with the new " + "network. \"Network Address\" must be specified. " + "\n\n" + "If you are creating a network WITHOUT a subnet, " + "clear \"Create Subnet\" checkbox.") + + def clean(self): + cleaned_data = super(CreateSubnetInfoAction, self).clean() + with_subnet = cleaned_data.get('with_subnet') + cidr = cleaned_data.get('cidr') + ip_version = int(cleaned_data.get('ip_version')) + gateway_ip = cleaned_data.get('gateway_ip') + if with_subnet and not cidr: + msg = _('Specify "Network Address" or ' + 'clear "Create Subnet" checkbox.') + raise forms.ValidationError(msg) + if cidr: + if netaddr.IPNetwork(cidr).version is not ip_version: + msg = _('Network Address and IP version are inconsistent.') + raise forms.ValidationError(msg) + if gateway_ip: + if netaddr.IPAddress(gateway_ip).version is not ip_version: + msg = _('Gateway IP and IP version are inconsistent.') + raise forms.ValidationError(msg) + return cleaned_data + + +class CreateSubnetInfo(workflows.Step): + action_class = CreateSubnetInfoAction + contributes = ("with_subnet", "subnet_name", "cidr", + "ip_version", "gateway_ip") + + +class CreateNetwork(workflows.Workflow): + slug = "create_network" + name = _("Create Network") + finalize_button_name = _("Create") + success_message = _('Created new network "%s".') + failure_message = _('Unable to create network "%s".') + success_url = "horizon:nova:networks:index" + default_steps = (CreateNetworkInfo, + CreateSubnetInfo) + + def format_status_message(self, message): + name = self.context.get('net_name') or self.context.get('net_id', '') + return message % name + + def handle(self, request, data): + # create the network + try: + network = api.quantum.network_create(request, + name=data['net_name']) + network.set_id_as_name_if_empty() + self.context['net_id'] = network.id + msg = _('Network %s was successfully created.') % network.name + LOG.debug(msg) + except: + msg = _('Failed to create network %s') % data['net_name'] + LOG.info(msg) + redirect = reverse('horizon:nova:networks:index') + exceptions.handle(request, msg, redirect=redirect) + return False + + # If we do not need to create a subnet, return here. + if not data['with_subnet']: + return True + + # create the subnet + try: + params = {'network_id': network.id, + 'name': data['subnet_name'], + 'cidr': data['cidr'], + 'ip_version': int(data['ip_version'])} + if data['gateway_ip']: + params['gateway_ip'] = data['gateway_ip'] + api.quantum.subnet_create(request, **params) + msg = _('Subnet %s was successfully created.') % data['cidr'] + LOG.debug(msg) + except Exception: + msg = _('Failed to create subnet %s for network %s') % \ + (data['cidr'], network.id) + LOG.info(msg) + redirect = reverse('horizon:nova:networks:index') + exceptions.handle(request, msg, redirect=redirect) + return False + + return True diff --git a/horizon/dashboards/syspanel/dashboard.py b/horizon/dashboards/syspanel/dashboard.py index bc2f10c9e1..a25c9fca43 100644 --- a/horizon/dashboards/syspanel/dashboard.py +++ b/horizon/dashboards/syspanel/dashboard.py @@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup): slug = "syspanel" name = _("System Panel") panels = ('overview', 'instances', 'volumes', 'services', 'flavors', - 'images', 'projects', 'users', 'quotas',) + 'images', 'projects', 'users', 'quotas', 'networks',) class Syspanel(horizon.Dashboard): diff --git a/horizon/dashboards/syspanel/instances/tests.py b/horizon/dashboards/syspanel/instances/tests.py index f0323707b5..e5db7598ef 100644 --- a/horizon/dashboards/syspanel/instances/tests.py +++ b/horizon/dashboards/syspanel/instances/tests.py @@ -143,6 +143,7 @@ class InstanceViewTest(test.BaseAdminViewTests): 'security_group_list', 'volume_list', 'volume_snapshot_list', 'tenant_quota_usages', 'server_create'), + api.quantum: ('network_list',), api.glance: ('image_list_detailed',)}) def test_launch_post(self): flavor = self.flavors.first() @@ -155,6 +156,7 @@ class InstanceViewTest(test.BaseAdminViewTests): device_name = u'vda' volume_choice = "%s:vol" % volume.id block_device_mapping = {device_name: u"%s::0" % volume_choice} + nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -171,6 +173,8 @@ class InstanceViewTest(test.BaseAdminViewTests): api.nova.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) api.nova.server_create(IsA(http.HttpRequest), server.name, image.id, @@ -179,6 +183,7 @@ class InstanceViewTest(test.BaseAdminViewTests): customization_script, [sec_group.name], block_device_mapping, + nics=nics, instance_count=IsA(int)) self.mox.ReplayAll() @@ -194,6 +199,7 @@ class InstanceViewTest(test.BaseAdminViewTests): 'volume_type': 'volume_id', 'volume_id': volume_choice, 'device_name': device_name, + 'network': self.networks.first().id, 'count': 1} url = reverse('horizon:syspanel:instances:launch') res = self.client.post(url, form_data) diff --git a/horizon/dashboards/syspanel/networks/__init__.py b/horizon/dashboards/syspanel/networks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/syspanel/networks/forms.py b/horizon/dashboards/syspanel/networks/forms.py new file mode 100644 index 0000000000..88bff09cf2 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/forms.py @@ -0,0 +1,67 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import messages + +from horizon.dashboards.nova.networks import forms as user_forms + + +LOG = logging.getLogger(__name__) + + +class CreateNetwork(forms.SelfHandlingForm): + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + tenant_id = forms.ChoiceField(label=_("Project")) + + @classmethod + def _instantiate(cls, request, *args, **kwargs): + return cls(request, *args, **kwargs) + + def __init__(self, request, *args, **kwargs): + super(CreateNetwork, self).__init__(request, *args, **kwargs) + tenant_choices = [('', _("Select a project"))] + for tenant in api.keystone.tenant_list(request, admin=True): + if tenant.enabled: + tenant_choices.append((tenant.id, tenant.name)) + self.fields['tenant_id'].choices = tenant_choices + + def handle(self, request, data): + try: + network = api.quantum.network_create(request, + name=data['name'], + tenant_id=data['tenant_id']) + msg = _('Network %s was successfully created.') % data['name'] + LOG.debug(msg) + messages.success(request, msg) + return network + except: + redirect = reverse('horizon:syspanel:networks:index') + msg = _('Failed to create network %s') % data['name'] + exceptions.handle(request, msg, redirect=redirect) + + +class UpdateNetwork(user_forms.UpdateNetwork): + failure_url = 'horizon:syspanel:networks:index' diff --git a/horizon/dashboards/syspanel/networks/panel.py b/horizon/dashboards/syspanel/networks/panel.py new file mode 100644 index 0000000000..638c2bed1d --- /dev/null +++ b/horizon/dashboards/syspanel/networks/panel.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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_lazy as _ + +import horizon +from horizon.dashboards.syspanel import dashboard + + +class Networks(horizon.Panel): + name = _("Networks") + slug = 'networks' + permissions = ('openstack.services.network',) + +dashboard.Syspanel.register(Networks) diff --git a/horizon/dashboards/syspanel/networks/ports/__init__.py b/horizon/dashboards/syspanel/networks/ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/syspanel/networks/ports/forms.py b/horizon/dashboards/syspanel/networks/ports/forms.py new file mode 100644 index 0000000000..2e86ed1370 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/ports/forms.py @@ -0,0 +1,92 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import messages + + +LOG = logging.getLogger(__name__) + + +class CreatePort(forms.SelfHandlingForm): + network_name = forms.CharField(label=_("Network Name"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + network_id = forms.CharField(label=_("Network ID"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + device_id = forms.CharField(max_length=100, label=_("Device ID"), + help_text='Device ID attached to the port', + required=False) + + def handle(self, request, data): + try: + # We must specify tenant_id of the network which a subnet is + # created for if admin user does not belong to the tenant. + network = api.quantum.network_get(request, data['network_id']) + data['tenant_id'] = network.tenant_id + + port = api.quantum.port_create(request, **data) + msg = _('Port %s was successfully created.') % port['id'] + LOG.debug(msg) + messages.success(request, msg) + return port + except: + msg = _('Failed to create a port for network %s') \ + % data['network_id'] + LOG.info(msg) + redirect = reverse('horizon:syspanel:networks:detail', + args=(data['network_id'],)) + exceptions.handle(request, msg, redirect=redirect) + + +class UpdatePort(forms.SelfHandlingForm): + network_id = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + port_id = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(max_length=255, + label=_("Name"), + required=False) + device_id = forms.CharField(max_length=100, label=_("Device ID"), + help_text='Device ID attached to the port', + required=False) + + def handle(self, request, data): + try: + LOG.debug('params = %s' % data) + port = api.quantum.port_modify(request, data['port_id'], + name=data['name'], + device_id=data['device_id']) + msg = _('Port %s was successfully updated.') % data['port_id'] + LOG.debug(msg) + messages.success(request, msg) + return port + except Exception: + msg = _('Failed to update port %s') % data['port_id'] + LOG.info(msg) + redirect = reverse('horizon:syspanel:networks:detail', + args=[data['network_id']]) + exceptions.handle(request, msg, redirect=redirect) diff --git a/horizon/dashboards/syspanel/networks/ports/tables.py b/horizon/dashboards/syspanel/networks/ports/tables.py new file mode 100644 index 0000000000..824d6703cd --- /dev/null +++ b/horizon/dashboards/syspanel/networks/ports/tables.py @@ -0,0 +1,85 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tables + +from horizon.dashboards.nova.networks.ports.tables import (get_fixed_ips, + get_attached) + + +LOG = logging.getLogger(__name__) + + +class DeletePort(tables.DeleteAction): + data_type_singular = _("Port") + data_type_plural = _("Ports") + + def delete(self, request, obj_id): + try: + api.quantum.port_delete(request, obj_id) + except: + msg = _('Failed to delete subnet %s') % obj_id + LOG.info(msg) + network_id = self.table.kwargs['network_id'] + redirect = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + exceptions.handle(request, msg, redirect=redirect) + + +class CreatePort(tables.LinkAction): + name = "create" + verbose_name = _("Create Port") + url = "horizon:syspanel:networks:addport" + classes = ("ajax-modal", "btn-create") + + def get_link_url(self, datum=None): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id,)) + + +class UpdatePort(tables.LinkAction): + name = "update" + verbose_name = _("Edit Port") + url = "horizon:syspanel:networks:editport" + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, port): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id, port.id)) + + +class PortsTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name"), + link="horizon:syspanel:networks:ports:detail") + fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs")) + device_id = tables.Column(get_attached, verbose_name=_("Device Attached")) + status = tables.Column("status", verbose_name=_("Status")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + class Meta: + name = "ports" + verbose_name = _("Ports") + table_actions = (CreatePort, DeletePort) + row_actions = (UpdatePort, DeletePort,) diff --git a/horizon/dashboards/syspanel/networks/ports/tabs.py b/horizon/dashboards/syspanel/networks/ports/tabs.py new file mode 100644 index 0000000000..a1f723e09b --- /dev/null +++ b/horizon/dashboards/syspanel/networks/ports/tabs.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tabs + +import logging +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = "nova/networks/ports/_detail_overview.html" + + def get_context_data(self, request): + port_id = self.tab_group.kwargs['port_id'] + try: + port = api.quantum.port_get(self.request, port_id) + except: + redirect = reverse('horizon:syspanel:networks:index') + msg = _('Unable to retrieve port details.') + exceptions.handle(request, msg, redirect=redirect) + return {'port': port} + + +class PortDetailTabs(tabs.TabGroup): + slug = "port_details" + tabs = (OverviewTab,) diff --git a/horizon/dashboards/syspanel/networks/ports/urls.py b/horizon/dashboards/syspanel/networks/ports/urls.py new file mode 100644 index 0000000000..5bac3a03d2 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/ports/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 + +from horizon.dashboards.nova.networks.ports.views import DetailView + +PORTS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.syspanel.networks.ports.views', + url(PORTS % 'detail', DetailView.as_view(), name='detail')) diff --git a/horizon/dashboards/syspanel/networks/ports/views.py b/horizon/dashboards/syspanel/networks/ports/views.py new file mode 100644 index 0000000000..9ac57de619 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/ports/views.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from .forms import CreatePort, UpdatePort + +LOG = logging.getLogger(__name__) + + +class CreateView(forms.ModalFormView): + form_class = CreatePort + template_name = 'syspanel/networks/ports/create.html' + success_url = 'horizon:syspanel:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def get_object(self): + if not hasattr(self, "_object"): + try: + network_id = self.kwargs["network_id"] + self._object = api.quantum.network_get(self.request, + network_id) + except: + redirect = reverse("horizon:syspanel:networks:detail", + args=(self.kwargs['network_id'],)) + msg = _("Unable to retrieve network.") + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(CreateView, self).get_context_data(**kwargs) + context['network'] = self.get_object() + return context + + def get_initial(self): + network = self.get_object() + return {"network_id": self.kwargs['network_id'], + "network_name": network.name} + + +class UpdateView(forms.ModalFormView): + form_class = UpdatePort + template_name = 'syspanel/networks/ports/update.html' + context_object_name = 'port' + success_url = 'horizon:syspanel:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def _get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + port_id = self.kwargs['port_id'] + try: + self._object = api.quantum.port_get(self.request, port_id) + except: + redirect = reverse("horizon:syspanel:networks:detail", + args=(self.kwargs['network_id'],)) + msg = _('Unable to retrieve port details') + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + port = self._get_object() + context['port_id'] = port['id'] + context['network_id'] = port['network_id'] + return context + + def get_initial(self): + port = self._get_object() + return {'port_id': port['id'], + 'network_id': port['network_id'], + 'tenant_id': port['tenant_id'], + 'name': port['name'], + 'device_id': port['device_id']} diff --git a/horizon/dashboards/syspanel/networks/subnets/__init__.py b/horizon/dashboards/syspanel/networks/subnets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/horizon/dashboards/syspanel/networks/subnets/forms.py b/horizon/dashboards/syspanel/networks/subnets/forms.py new file mode 100644 index 0000000000..9727831e92 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/subnets/forms.py @@ -0,0 +1,52 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import forms +from horizon import exceptions + +from horizon.dashboards.nova.networks.subnets import forms as user_forms + + +LOG = logging.getLogger(__name__) + + +class CreateSubnet(user_forms.CreateSubnet): + failure_url = 'horizon:syspanel:networks:detail' + + def handle(self, request, data): + try: + # We must specify tenant_id of the network which a subnet is + # created for if admin user does not belong to the tenant. + network = api.quantum.network_get(request, data['network_id']) + data['tenant_id'] = network.tenant_id + except: + msg = _('Failed to retrieve network %s for a subnet') \ + % data['network_id'] + LOG.info(msg) + redirect = reverse(self.failure_url, args=[data['network_id']]) + exceptions.handle(request, msg, redirect=redirect) + return super(CreateSubnet, self).handle(request, data) + + +class UpdateSubnet(user_forms.UpdateSubnet): + tenant_id = forms.CharField(widget=forms.HiddenInput()) + failure_url = 'horizon:syspanel:networks:detail' diff --git a/horizon/dashboards/syspanel/networks/subnets/tables.py b/horizon/dashboards/syspanel/networks/subnets/tables.py new file mode 100644 index 0000000000..20c740141c --- /dev/null +++ b/horizon/dashboards/syspanel/networks/subnets/tables.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class DeleteSubnet(tables.DeleteAction): + data_type_singular = _("Subnet") + data_type_plural = _("Subnets") + + def delete(self, request, obj_id): + try: + api.quantum.subnet_delete(request, obj_id) + except: + msg = _('Failed to delete subnet %s') % obj_id + LOG.info(msg) + network_id = self.table.kwargs['network_id'] + redirect = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + exceptions.handle(request, msg, redirect=redirect) + + +class CreateSubnet(tables.LinkAction): + name = "create" + verbose_name = _("Create Subnet") + url = "horizon:syspanel:networks:addsubnet" + classes = ("ajax-modal", "btn-create") + + def get_link_url(self, datum=None): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id,)) + + +class UpdateSubnet(tables.LinkAction): + name = "update" + verbose_name = _("Edit Subnet") + url = "horizon:syspanel:networks:editsubnet" + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, subnet): + network_id = self.table.kwargs['network_id'] + return reverse(self.url, args=(network_id, subnet.id)) + + +class SubnetsTable(tables.DataTable): + name = tables.Column("name", verbose_name=_("Name"), + link='horizon:syspanel:networks:subnets:detail') + cidr = tables.Column("cidr", verbose_name=_("CIDR")) + ip_version = tables.Column("ipver_str", verbose_name=_("IP Version")) + gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP")) + + def get_object_display(self, subnet): + return subnet.id + + class Meta: + name = "subnets" + verbose_name = _("Subnets") + table_actions = (CreateSubnet, DeleteSubnet) + row_actions = (UpdateSubnet, DeleteSubnet,) diff --git a/horizon/dashboards/syspanel/networks/subnets/urls.py b/horizon/dashboards/syspanel/networks/subnets/urls.py new file mode 100644 index 0000000000..7017e8f1bb --- /dev/null +++ b/horizon/dashboards/syspanel/networks/subnets/urls.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 + +from horizon.dashboards.nova.networks.subnets.views import DetailView + +SUBNETS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('horizon.dashboards.syspanel.networks.subnets.views', + url(SUBNETS % 'detail', DetailView.as_view(), name='detail')) diff --git a/horizon/dashboards/syspanel/networks/subnets/views.py b/horizon/dashboards/syspanel/networks/subnets/views.py new file mode 100644 index 0000000000..0c5a0c1edd --- /dev/null +++ b/horizon/dashboards/syspanel/networks/subnets/views.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import forms +from .forms import CreateSubnet, UpdateSubnet + +LOG = logging.getLogger(__name__) + + +class CreateView(forms.ModalFormView): + form_class = CreateSubnet + template_name = 'syspanel/networks/subnets/create.html' + success_url = 'horizon:syspanel:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def get_object(self): + if not hasattr(self, "_object"): + try: + network_id = self.kwargs["network_id"] + self._object = api.quantum.network_get(self.request, + network_id) + except: + redirect = reverse('horizon:nova:networks:index') + msg = _("Unable to retrieve network.") + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(CreateView, self).get_context_data(**kwargs) + context['network'] = self.get_object() + return context + + def get_initial(self): + network = self.get_object() + return {"network_id": self.kwargs['network_id'], + "network_name": network.name} + + +class UpdateView(forms.ModalFormView): + form_class = UpdateSubnet + template_name = 'syspanel/networks/subnets/update.html' + context_object_name = 'subnet' + success_url = 'horizon:syspanel:networks:detail' + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['network_id'],)) + + def _get_object(self, *args, **kwargs): + if not hasattr(self, "_object"): + subnet_id = self.kwargs['subnet_id'] + try: + self._object = api.quantum.subnet_get(self.request, subnet_id) + except: + redirect = reverse("horizon:syspanel:networks:detail", + args=(self.kwargs['network_id'],)) + msg = _('Unable to retrieve subnet details') + exceptions.handle(self.request, msg, redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + subnet = self._get_object() + context['subnet_id'] = subnet['id'] + context['network_id'] = subnet['network_id'] + context['cidr'] = subnet['cidr'] + context['ip_version'] = {4: 'IPv4', 6: 'IPv6'}[subnet['ip_version']] + return context + + def get_initial(self): + subnet = self._get_object() + return {'network_id': self.kwargs['network_id'], + 'subnet_id': subnet['id'], + 'tenant_id': subnet['tenant_id'], + 'cidr': subnet['cidr'], + 'ip_version': subnet['ip_version'], + 'name': subnet['name'], + 'gateway_ip': subnet['gateway_ip']} diff --git a/horizon/dashboards/syspanel/networks/tables.py b/horizon/dashboards/syspanel/networks/tables.py new file mode 100644 index 0000000000..77ba968a11 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/tables.py @@ -0,0 +1,79 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import api +from horizon import exceptions +from horizon import tables + +from horizon.dashboards.nova.networks.tables import get_subnets + + +LOG = logging.getLogger(__name__) + + +class DeleteNetwork(tables.DeleteAction): + data_type_singular = _("Network") + data_type_plural = _("Networks") + + def delete(self, request, obj_id): + try: + api.quantum.network_delete(request, obj_id) + except: + msg = _('Failed to delete network %s') % obj_id + LOG.info(msg) + redirect = reverse('horizon:syspanel:networks:index') + exceptions.handle(request, msg, redirect=redirect) + + +class CreateNetwork(tables.LinkAction): + name = "create" + verbose_name = _("Create Network") + url = "horizon:syspanel:networks:create" + classes = ("ajax-modal", "btn-create") + + +class EditNetwork(tables.LinkAction): + name = "update" + verbose_name = _("Edit Network") + url = "horizon:syspanel:networks:update" + classes = ("ajax-modal", "btn-edit") + + +#def _get_subnets(network): +# cidrs = [subnet.get('cidr') for subnet in network.subnets] +# return ','.join(cidrs) + + +class NetworksTable(tables.DataTable): + tenant = tables.Column("tenant_name", verbose_name=_("Project")) + name = tables.Column("name", verbose_name=_("Network Name"), + link='horizon:syspanel:networks:detail') + subnets = tables.Column(get_subnets, + verbose_name=_("Subnets Associated"),) + status = tables.Column("status", verbose_name=_("Status")) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State")) + + class Meta: + name = "networks" + verbose_name = _("Networks") + table_actions = (CreateNetwork, DeleteNetwork) + row_actions = (EditNetwork, DeleteNetwork) diff --git a/horizon/dashboards/syspanel/networks/templates/networks/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/_create.html new file mode 100644 index 0000000000..7b35a32136 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_network_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:create %}{% endblock %} + +{% block modal_id %}create_network_modal{% endblock %} +{% block modal-header %}{% trans "Create Network" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "Select a name for your network."%}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/_update.html new file mode 100644 index 0000000000..63d5c2fba8 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/_update.html @@ -0,0 +1,24 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}update_network_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:update network_id %}{% endblock %} + +{% block modal-header %}{% trans "Edit Network" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "You may update the editable properties of your network here." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/create.html b/horizon/dashboards/syspanel/networks/templates/networks/create.html new file mode 100644 index 0000000000..c39cc08592 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Network" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Network") %} +{% endblock page_header %} + +{% block main %} + {% include "syspanel/networks/_create.html" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/index.html b/horizon/dashboards/syspanel/networks/templates/networks/index.html new file mode 100644 index 0000000000..bcb1b74f89 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/index.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Networks" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Networks") %} +{% endblock page_header %} + +{% block main %} +
+ {{ networks_table.render }} +
+ +
+ {{ subnets_table.render }} +
+ +
+ {{ ports_table.render }} +
+{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html new file mode 100644 index 0000000000..ff6549363e --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_port_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:addport network.id %} +{% endblock %} + +{% block modal-header %}{% trans "Create Port" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "You can create a port for the network. If you specify device ID to be attached, the device specified will be attached to the port created."%}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html new file mode 100644 index 0000000000..6481c2a690 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html @@ -0,0 +1,29 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}update_port_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:editport network_id port_id %}{% endblock %} + +{% block modal-header %}{% trans "Edit Port" %}{% endblock %} + +{% block modal-body %} +
+
+
{% trans "ID" %}
+
{{ port_id }}
+
+
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "You may update the editable properties of your port here." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html new file mode 100644 index 0000000000..76f1019c5c --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Port" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Port") %} +{% endblock page_header %} + +{% block main %} + {% include "syspanel/networks/ports/_create.html" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html new file mode 100644 index 0000000000..33b64d89ab --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Port" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Port") %} +{% endblock page_header %} + +{% block main %} + {% include 'syspanel/networks/ports/_update.html' %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html new file mode 100644 index 0000000000..1e16b900fe --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_subnet_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:addsubnet network.id %} +{% endblock %} + +{% block modal-header %}{% trans "Create Subnet" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "You can create a subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html new file mode 100644 index 0000000000..e7a41af7f9 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html @@ -0,0 +1,33 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}update_subnet_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:networks:editsubnet network_id subnet_id %}{% endblock %} + +{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %} + +{% block modal-body %} +
+
+
{% trans "ID" %}
+
{{ subnet_id }}
+
{% trans "Network Address" %}
+
{{ cidr }}
+
{% trans "IP version" %}
+
{{ ip_version }}
+
+
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "You may update the editable properties of your subnet here." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html new file mode 100644 index 0000000000..59bd53050c --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Subnet" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Subnet") %} +{% endblock page_header %} + +{% block main %} + {% include "syspanel/networks/subnets/_create.html" %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html new file mode 100644 index 0000000000..9c25d565e2 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Network Detail" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Network Detail") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html new file mode 100644 index 0000000000..92dbff0c7c --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Subnet" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Subnet") %} +{% endblock page_header %} + +{% block main %} + {% include 'syspanel/networks/subnets/_update.html' %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/templates/networks/update.html b/horizon/dashboards/syspanel/networks/templates/networks/update.html new file mode 100644 index 0000000000..9870a3652e --- /dev/null +++ b/horizon/dashboards/syspanel/networks/templates/networks/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Network" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Network") %} +{% endblock page_header %} + +{% block main %} + {% include 'syspanel/networks/_update.html' %} +{% endblock %} diff --git a/horizon/dashboards/syspanel/networks/tests.py b/horizon/dashboards/syspanel/networks/tests.py new file mode 100644 index 0000000000..9bbc5fa3ba --- /dev/null +++ b/horizon/dashboards/syspanel/networks/tests.py @@ -0,0 +1,801 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse +from django.utils.html import escape +from mox import IsA + +from horizon import api +from horizon import test + + +INDEX_URL = reverse('horizon:syspanel:networks:index') + + +class NetworkTests(test.BaseAdminViewTests): + @test.create_stubs({api.quantum: ('network_list',), + api.keystone: ('tenant_list',)}) + def test_index(self): + tenants = self.tenants.list() + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndReturn(self.networks.list()) + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'syspanel/networks/index.html') + networks = res.context['networks_table'].data + self.assertItemsEqual(networks, self.networks.list()) + + @test.create_stubs({api.quantum: ('network_list',)}) + def test_index_network_list_exception(self): + api.quantum.network_list(IsA(http.HttpRequest)) \ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'syspanel/networks/index.html') + self.assertEqual(len(res.context['networks_table'].data), 0) + self.assertMessageCount(res, error=1) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertItemsEqual(subnets, [self.subnets.first()]) + self.assertItemsEqual(ports, [self.ports.first()]) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_network_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:detail', args=[network_id]) + res = self.client.get(url) + + redir_url = INDEX_URL + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_subnet_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id).\ + AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\ + AndRaise(self.exceptions.quantum) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\ + AndReturn([self.ports.first()]) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertEqual(len(subnets), 0) + self.assertItemsEqual(ports, [self.ports.first()]) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_list', + 'port_list',)}) + def test_network_detail_port_exception(self): + network_id = self.networks.first().id + api.quantum.network_get(IsA(http.HttpRequest), network_id).\ + AndReturn(self.networks.first()) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\ + AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\ + AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:networks:detail', + args=[network_id])) + + self.assertTemplateUsed(res, 'nova/networks/detail.html') + subnets = res.context['subnets_table'].data + ports = res.context['ports_table'].data + self.assertItemsEqual(subnets, [self.subnets.first()]) + self.assertEqual(len(ports), 0) + + @test.create_stubs({api.keystone: ('tenant_list',)}) + def test_network_create_get(self): + tenants = self.tenants.list() + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:create') + res = self.client.get(url) + + self.assertTemplateUsed(res, 'syspanel/networks/create.html') + + @test.create_stubs({api.quantum: ('network_create',), + api.keystone: ('tenant_list',)}) + def test_network_create_post(self): + tenants = self.tenants.list() + tenant_id = self.tenants.first().id + network = self.networks.first() + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + tenant_id=tenant_id)\ + .AndReturn(network) + self.mox.ReplayAll() + + form_data = {'tenant_id': tenant_id, + 'name': network.name} + url = reverse('horizon:syspanel:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_create',), + api.keystone: ('tenant_list',)}) + def test_network_create_post_network_exception(self): + tenants = self.tenants.list() + tenant_id = self.tenants.first().id + network = self.networks.first() + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + api.quantum.network_create(IsA(http.HttpRequest), name=network.name, + tenant_id=tenant_id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'tenant_id': tenant_id, + 'name': network.name} + url = reverse('horizon:syspanel:networks:create') + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_network_update_get(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:update', args=[network.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'syspanel/networks/update.html') + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_network_update_get_exception(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:update', args=[network.id]) + res = self.client.get(url) + + redir_url = INDEX_URL + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_modify', + 'network_get',)}) + def test_network_update_post(self): + network = self.networks.first() + api.quantum.network_modify(IsA(http.HttpRequest), network.id, + name=network.name)\ + .AndReturn(network) + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + self.mox.ReplayAll() + + formData = {'network_id': network.id, + 'name': network.name, + 'tenant_id': network.tenant_id} + url = reverse('horizon:syspanel:networks:update', args=[network.id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_modify', + 'network_get',)}) + def test_network_update_post_exception(self): + network = self.networks.first() + api.quantum.network_modify(IsA(http.HttpRequest), network.id, + name=network.name)\ + .AndRaise(self.exceptions.quantum) + api.quantum.network_get(IsA(http.HttpRequest), network.id)\ + .AndReturn(network) + self.mox.ReplayAll() + + form_data = {'network_id': network.id, + 'name': network.name, + 'tenant_id': network.tenant_id} + url = reverse('horizon:syspanel:networks:update', args=[network.id]) + res = self.client.post(url, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list', + 'network_delete'), + api.keystone: ('tenant_list',)}) + def test_delete_network(self): + tenants = self.tenants.list() + network = self.networks.first() + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + api.quantum.network_list(IsA(http.HttpRequest))\ + .AndReturn([network]) + api.quantum.network_delete(IsA(http.HttpRequest), network.id) + + self.mox.ReplayAll() + + form_data = {'action': 'networks__delete__%s' % network.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('network_list', + 'network_delete'), + api.keystone: ('tenant_list',)}) + def test_delete_network_exception(self): + tenants = self.tenants.list() + network = self.networks.first() + api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\ + .AndReturn(tenants) + api.quantum.network_list(IsA(http.HttpRequest))\ + .AndReturn([network]) + api.quantum.network_delete(IsA(http.HttpRequest), network.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + form_data = {'action': 'networks__delete__%s' % network.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.quantum: ('subnet_get',)}) + def test_subnet_detail(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(self.subnets.first()) + + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:subnets:detail', + args=[subnet.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'nova/networks/subnets/detail.html') + self.assertEqual(res.context['subnet'].id, subnet.id) + + @test.create_stubs({api.quantum: ('subnet_get',)}) + def test_subnet_detail_exception(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:subnets:detail', + args=[subnet.id]) + res = self.client.get(url) + + # syspanel DetailView is shared with userpanel one, so + # redirection URL on error is userpanel index. + redir_url = reverse('horizon:nova:networks:index') + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_get(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:addsubnet', + args=[network.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'syspanel/networks/subnets/create.html') + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + network_name=network.name, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip, + tenant_id=subnet.tenant_id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:syspanel:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:syspanel:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_network_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:syspanel:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + # syspanel DetailView is shared with userpanel one, so + # redirection URL on error is userpanel index. + redir_url = reverse('horizon:nova:networks:index') + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'subnet_create',)}) + def test_subnet_create_post_subnet_exception(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.subnet_create(IsA(http.HttpRequest), + network_id=network.id, + network_name=network.name, + name=subnet.name, + cidr=subnet.cidr, + ip_version=subnet.ip_version, + gateway_ip=subnet.gateway_ip, + tenant_id=subnet.tenant_id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:syspanel:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + redir_url = reverse('horizon:syspanel:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_cidr_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + # dummy IPv6 address + cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60' + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:syspanel:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + expected_msg = 'Network Address and IP version are inconsistent.' + self.assertContains(res, expected_msg) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_subnet_create_post_gw_inconsistent(self): + network = self.networks.first() + subnet = self.subnets.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + # dummy IPv6 address + gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' + form_data = {'network_id': subnet.network_id, + 'network_name': network.name, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': gateway_ip} + url = reverse('horizon:syspanel:networks:addsubnet', + args=[subnet.network_id]) + res = self.client.post(url, form_data) + + self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id, + name=subnet.name, + gateway_ip=subnet.gateway_ip)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + formData = {'network_id': subnet.network_id, + 'tenant_id': subnet.tenant_id, + 'subnet_id': subnet.id, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': subnet.gateway_ip} + url = reverse('horizon:syspanel:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, formData) + + redir_url = reverse('horizon:syspanel:networks:detail', + args=[subnet.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('subnet_modify', + 'subnet_get',)}) + def test_subnet_update_post_gw_inconsistent(self): + subnet = self.subnets.first() + api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\ + .AndReturn(subnet) + self.mox.ReplayAll() + + # dummy IPv6 address + gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF' + formData = {'network_id': subnet.network_id, + 'tenant_id': subnet.tenant_id, + 'subnet_id': subnet.id, + 'name': subnet.name, + 'cidr': subnet.cidr, + 'ip_version': subnet.ip_version, + 'gateway_ip': gateway_ip} + url = reverse('horizon:syspanel:networks:editsubnet', + args=[subnet.network_id, subnet.id]) + res = self.client.post(url, formData) + + self.assertContains(res, 'Gateway IP and IP version are inconsistent.') + + @test.create_stubs({api.quantum: ('subnet_delete', + 'subnet_list', + 'port_list',)}) + def test_subnet_delete(self): + subnet = self.subnets.first() + network_id = subnet.network_id + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'subnets__delete__%s' % subnet.id} + url = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.quantum: ('subnet_delete', + 'subnet_list', + 'port_list',)}) + def test_subnet_delete_exception(self): + subnet = self.subnets.first() + network_id = subnet.network_id + api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'subnets__delete__%s' % subnet.id} + url = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_detail(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(self.ports.first()) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:networks:ports:detail', + args=[port.id])) + + self.assertTemplateUsed(res, 'nova/networks/ports/detail.html') + self.assertEqual(res.context['port'].id, port.id) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_detail_exception(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndRaise(self.exceptions.quantum) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:syspanel:networks:ports:detail', + args=[port.id])) + + # syspanel DetailView is shared with userpanel one, so + # redirection URL on error is userpanel index. + redir_url = reverse('horizon:nova:networks:index') + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get',)}) + def test_port_create_get(self): + network = self.networks.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:addport', + args=[network.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'syspanel/networks/ports/create.html') + + @test.create_stubs({api.quantum: ('network_get', + 'port_create')}) + def test_port_create_post(self): + network = self.networks.first() + port = self.ports.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.port_create(IsA(http.HttpRequest), + tenant_id=network.tenant_id, + network_id=network.id, + network_name=network.name, + name=port.name, + device_id=port.device_id)\ + .AndReturn(port) + self.mox.ReplayAll() + + form_data = {'network_id': port.network_id, + 'network_name': network.name, + 'name': port.name, + 'device_id': port.device_id} + url = reverse('horizon:syspanel:networks:addport', + args=[port.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:syspanel:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('network_get', + 'port_create')}) + def test_port_create_post_exception(self): + network = self.networks.first() + port = self.ports.first() + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.quantum.port_create(IsA(http.HttpRequest), + tenant_id=network.tenant_id, + network_id=network.id, + network_name=network.name, + name=port.name, + device_id=port.device_id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + form_data = {'network_id': port.network_id, + 'network_name': network.name, + 'name': port.name, + 'device_id': port.device_id} + url = reverse('horizon:syspanel:networks:addport', + args=[port.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse('horizon:syspanel:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('port_get',)}) + def test_port_update_get(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), + port.id)\ + .AndReturn(port) + self.mox.ReplayAll() + + url = reverse('horizon:syspanel:networks:editport', + args=[port.network_id, port.id]) + res = self.client.get(url) + + self.assertTemplateUsed(res, 'syspanel/networks/ports/update.html') + + @test.create_stubs({api.quantum: ('port_get', + 'port_modify')}) + def test_port_update_post(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(port) + api.quantum.port_modify(IsA(http.HttpRequest), port.id, + name=port.name, device_id=port.device_id)\ + .AndReturn(port) + self.mox.ReplayAll() + + formData = {'tenant_id': port.tenant_id, + 'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'device_id': port.device_id} + url = reverse('horizon:syspanel:networks:editport', + args=[port.network_id, port.id]) + res = self.client.post(url, formData) + + redir_url = reverse('horizon:syspanel:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('port_get', + 'port_modify')}) + def test_port_update_post_exception(self): + port = self.ports.first() + api.quantum.port_get(IsA(http.HttpRequest), port.id)\ + .AndReturn(port) + api.quantum.port_modify(IsA(http.HttpRequest), port.id, + name=port.name, device_id=port.device_id)\ + .AndRaise(self.exceptions.quantum) + self.mox.ReplayAll() + + formData = {'tenant_id': port.tenant_id, + 'network_id': port.network_id, + 'port_id': port.id, + 'name': port.name, + 'device_id': port.device_id} + url = reverse('horizon:syspanel:networks:editport', + args=[port.network_id, port.id]) + res = self.client.post(url, formData) + + redir_url = reverse('horizon:syspanel:networks:detail', + args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + + @test.create_stubs({api.quantum: ('port_delete', + 'subnet_list', + 'port_list',)}) + def test_port_delete(self): + port = self.ports.first() + network_id = port.network_id + api.quantum.port_delete(IsA(http.HttpRequest), port.id) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'ports__delete__%s' % port.id} + url = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) + + @test.create_stubs({api.quantum: ('port_delete', + 'subnet_list', + 'port_list',)}) + def test_port_delete_exception(self): + port = self.ports.first() + network_id = port.network_id + api.quantum.port_delete(IsA(http.HttpRequest), port.id)\ + .AndRaise(self.exceptions.quantum) + api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.subnets.first()]) + api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\ + .AndReturn([self.ports.first()]) + self.mox.ReplayAll() + + formData = {'action': 'ports__delete__%s' % port.id} + url = reverse('horizon:syspanel:networks:detail', + args=[network_id]) + res = self.client.post(url, formData) + + self.assertRedirectsNoFollow(res, url) diff --git a/horizon/dashboards/syspanel/networks/urls.py b/horizon/dashboards/syspanel/networks/urls.py new file mode 100644 index 0000000000..efc77ce774 --- /dev/null +++ b/horizon/dashboards/syspanel/networks/urls.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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, include +from .views import IndexView, CreateView, DetailView, UpdateView + +from .subnets.views import CreateView as AddSubnetView +from .subnets.views import UpdateView as EditSubnetView +from .ports.views import CreateView as AddPortView +from .ports.views import UpdateView as EditPortView + +from .subnets import urls as subnet_urls +from .ports import urls as port_urls + +NETWORKS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create/$', CreateView.as_view(), name='create'), + url(NETWORKS % 'update', UpdateView.as_view(), name='update'), + # for detail view + url(NETWORKS % 'detail', DetailView.as_view(), name='detail'), + url(NETWORKS % 'subnets/create', AddSubnetView.as_view(), + name='addsubnet'), + url(NETWORKS % 'ports/create', AddPortView.as_view(), name='addport'), + url(r'^(?P[^/]+)/subnets/(?P[^/]+)/update$', + EditSubnetView.as_view(), name='editsubnet'), + url(r'^(?P[^/]+)/ports/(?P[^/]+)/update$', + EditPortView.as_view(), name='editport'), + + url(r'^subnets/', include(subnet_urls, namespace='subnets')), + url(r'^ports/', include(port_urls, namespace='ports'))) diff --git a/horizon/dashboards/syspanel/networks/views.py b/horizon/dashboards/syspanel/networks/views.py new file mode 100644 index 0000000000..6d61d2e16f --- /dev/null +++ b/horizon/dashboards/syspanel/networks/views.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict + +from horizon import api +from horizon import exceptions +from horizon import forms +from horizon import tables + +from .tables import NetworksTable +from .subnets.tables import SubnetsTable +from .ports.tables import PortsTable +from .forms import CreateNetwork, UpdateNetwork + +from horizon.dashboards.nova.networks import views as user_views + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = NetworksTable + template_name = 'syspanel/networks/index.html' + + def _get_tenant_list(self): + if not hasattr(self, "_tenants"): + try: + tenants = api.keystone.tenant_list(self.request, admin=True) + except: + tenants = [] + msg = _('Unable to retrieve instance tenant information.') + exceptions.handle(self.request, msg) + + tenant_dict = SortedDict([(t.id, t) for t in tenants]) + self._tenants = tenant_dict + return self._tenants + + def get_data(self): + try: + networks = api.quantum.network_list(self.request) + except: + networks = [] + msg = _('Network list can not be retrieved.') + exceptions.handle(self.request, msg) + if networks: + tenant_dict = self._get_tenant_list() + for n in networks: + # Set tenant name + tenant = tenant_dict.get(n.tenant_id, None) + n.tenant_name = getattr(tenant, 'name', None) + # If name is empty use UUID as name + n.set_id_as_name_if_empty() + return networks + + +class CreateView(forms.ModalFormView): + form_class = CreateNetwork + template_name = 'syspanel/networks/create.html' + success_url = reverse_lazy('horizon:syspanel:networks:index') + + +class DetailView(tables.MultiTableView): + table_classes = (SubnetsTable, PortsTable) + template_name = 'nova/networks/detail.html' + failure_url = reverse_lazy('horizon:syspanel:networks:index') + + def get_subnets_data(self): + try: + network_id = self.kwargs['network_id'] + subnets = api.quantum.subnet_list(self.request, + network_id=network_id) + except: + subnets = [] + msg = _('Subnet list can not be retrieved.') + exceptions.handle(self.request, msg) + for s in subnets: + s.set_id_as_name_if_empty() + return subnets + + def get_ports_data(self): + try: + network_id = self.kwargs['network_id'] + ports = api.quantum.port_list(self.request, network_id=network_id) + except: + ports = [] + msg = _('Port list can not be retrieved.') + exceptions.handle(self.request, msg) + for p in ports: + p.set_id_as_name_if_empty() + return ports + + def _get_data(self): + if not hasattr(self, "_network"): + try: + network_id = self.kwargs['network_id'] + network = api.quantum.network_get(self.request, network_id) + network.set_id_as_name_if_empty(length=0) + except: + redirect = self.failure_url + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'network "%s".') % network_id, + redirect=redirect) + self._network = network + return self._network + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + context["network"] = self._get_data() + return context + + +class UpdateView(user_views.UpdateView): + form_class = UpdateNetwork + template_name = 'syspanel/networks/update.html' + success_url = reverse_lazy('horizon:syspanel:networks:index') diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 660868fb3d..49917465bc 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -734,7 +734,7 @@ class DataTableOptions(object): self.table_actions_template = \ 'horizon/common/_data_table_table_actions.html' self.context_var_name = unicode(getattr(options, - 'context_var_nam', + 'context_var_name', 'table')) self.actions_column = getattr(options, 'actions_column', diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html index 9713752f5c..34c4dd9707 100644 --- a/horizon/templates/horizon/common/_data_table.html +++ b/horizon/templates/horizon/common/_data_table.html @@ -48,7 +48,7 @@ More » {% endif %} - + {% endwith %} diff --git a/horizon/templates/horizon/common/_workflow_step.html b/horizon/templates/horizon/common/_workflow_step.html index 0c998875ec..17b6deff41 100644 --- a/horizon/templates/horizon/common/_workflow_step.html +++ b/horizon/templates/horizon/common/_workflow_step.html @@ -2,12 +2,12 @@ - +
- {{ step.get_help_text }} - {% include "horizon/common/_form_fields.html" %} + {{ step.get_help_text }} +
diff --git a/horizon/test.py b/horizon/test.py index f5ce995811..1b294eb920 100644 --- a/horizon/test.py +++ b/horizon/test.py @@ -35,6 +35,7 @@ from django.utils import unittest import glanceclient from keystoneclient.v2_0 import client as keystone_client from novaclient.v1_1 import client as nova_client +from quantumclient.v2_0 import client as quantum_client from selenium.webdriver.firefox.webdriver import WebDriver import httplib2 @@ -290,17 +291,20 @@ class APITestCase(TestCase): self._original_glanceclient = api.glance.glanceclient self._original_keystoneclient = api.keystone.keystoneclient self._original_novaclient = api.nova.novaclient + self._original_quantumclient = api.quantum.quantumclient # Replace the clients with our stubs. api.glance.glanceclient = lambda request: self.stub_glanceclient() api.keystone.keystoneclient = fake_keystoneclient api.nova.novaclient = lambda request: self.stub_novaclient() + api.quantum.quantumclient = lambda request: self.stub_quantumclient() def tearDown(self): super(APITestCase, self).tearDown() api.glance.glanceclient = self._original_glanceclient api.nova.novaclient = self._original_novaclient api.keystone.keystoneclient = self._original_keystoneclient + api.quantum.quantumclient = self._original_quantumclient def stub_novaclient(self): if not hasattr(self, "novaclient"): @@ -320,6 +324,12 @@ class APITestCase(TestCase): self.glanceclient = self.mox.CreateMock(glanceclient.Client) return self.glanceclient + def stub_quantumclient(self): + if not hasattr(self, "quantumclient"): + self.mox.StubOutWithMock(quantum_client, 'Client') + self.quantumclient = self.mox.CreateMock(quantum_client.Client) + return self.quantumclient + def stub_swiftclient(self, expected_calls=1): if not hasattr(self, "swiftclient"): self.mox.StubOutWithMock(swift_client, 'Connection') diff --git a/horizon/tests/api_tests/quantum_tests.py b/horizon/tests/api_tests/quantum_tests.py new file mode 100644 index 0000000000..09a51a7884 --- /dev/null +++ b/horizon/tests/api_tests/quantum_tests.py @@ -0,0 +1,205 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 NEC Corporation +# +# 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 horizon import api +from horizon import test + + +class QuantumApiTests(test.APITestCase): + def test_network_list(self): + networks = {'networks': self.api_networks.list()} + subnets = {'subnets': self.api_subnets.list()} + + quantumclient = self.stub_quantumclient() + quantumclient.list_networks().AndReturn(networks) + quantumclient.list_subnets().AndReturn(subnets) + self.mox.ReplayAll() + + ret_val = api.quantum.network_list(self.request) + for n in ret_val: + self.assertIsInstance(n, api.quantum.Network) + + def test_network_get(self): + network = {'network': self.api_networks.first()} + subnet = {'subnet': self.api_subnets.first()} + network_id = self.api_networks.first()['id'] + subnet_id = self.api_networks.first()['subnets'][0] + + quantumclient = self.stub_quantumclient() + quantumclient.show_network(network_id).AndReturn(network) + quantumclient.show_subnet(subnet_id).AndReturn(subnet) + self.mox.ReplayAll() + + ret_val = api.quantum.network_get(self.request, network_id) + self.assertIsInstance(ret_val, api.quantum.Network) + + def test_network_create(self): + network = {'network': self.api_networks.first()} + + quantumclient = self.stub_quantumclient() + form_data = {'network': {'name': 'net1'}} + quantumclient.create_network(body=form_data).AndReturn(network) + self.mox.ReplayAll() + + ret_val = api.quantum.network_create(self.request, name='net1') + self.assertIsInstance(ret_val, api.quantum.Network) + + def test_network_modify(self): + network = {'network': self.api_networks.first()} + network_id = self.api_networks.first()['id'] + + quantumclient = self.stub_quantumclient() + form_data = {'network': {'name': 'net1'}} + quantumclient.update_network(network_id, body=form_data)\ + .AndReturn(network) + self.mox.ReplayAll() + + ret_val = api.quantum.network_modify(self.request, network_id, + name='net1') + self.assertIsInstance(ret_val, api.quantum.Network) + + def test_network_delete(self): + network_id = self.api_networks.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.delete_network(network_id) + self.mox.ReplayAll() + + api.quantum.network_delete(self.request, network_id) + + def test_subnet_list(self): + subnets = {'subnets': self.api_subnets.list()} + + quantumclient = self.stub_quantumclient() + quantumclient.list_subnets().AndReturn(subnets) + self.mox.ReplayAll() + + ret_val = api.quantum.subnet_list(self.request) + for n in ret_val: + self.assertIsInstance(n, api.quantum.Subnet) + + def test_subnet_get(self): + subnet = {'subnet': self.api_subnets.first()} + subnet_id = self.api_subnets.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.show_subnet(subnet_id).AndReturn(subnet) + self.mox.ReplayAll() + + ret_val = api.quantum.subnet_get(self.request, subnet_id) + self.assertIsInstance(ret_val, api.quantum.Subnet) + + def test_subnet_create(self): + subnet_data = self.api_subnets.first() + params = {'network_id': subnet_data['network_id'], + 'tenant_id': subnet_data['tenant_id'], + 'name': subnet_data['name'], + 'cidr': subnet_data['cidr'], + 'ip_version': subnet_data['ip_version'], + 'gateway_ip': subnet_data['gateway_ip']} + + quantumclient = self.stub_quantumclient() + quantumclient.create_subnet(body={'subnet': params})\ + .AndReturn({'subnet': subnet_data}) + self.mox.ReplayAll() + + ret_val = api.quantum.subnet_create(self.request, **params) + self.assertIsInstance(ret_val, api.quantum.Subnet) + + def test_subnet_modify(self): + subnet_data = self.api_subnets.first() + subnet_id = subnet_data['id'] + params = {'name': subnet_data['name'], + 'gateway_ip': subnet_data['gateway_ip']} + + quantumclient = self.stub_quantumclient() + quantumclient.update_subnet(subnet_id, body={'subnet': params})\ + .AndReturn({'subnet': subnet_data}) + self.mox.ReplayAll() + + ret_val = api.quantum.subnet_modify(self.request, subnet_id, **params) + self.assertIsInstance(ret_val, api.quantum.Subnet) + + def test_subnet_delete(self): + subnet_id = self.api_subnets.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.delete_subnet(subnet_id) + self.mox.ReplayAll() + + api.quantum.subnet_delete(self.request, subnet_id) + + def test_port_list(self): + ports = {'ports': self.api_ports.list()} + + quantumclient = self.stub_quantumclient() + quantumclient.list_ports().AndReturn(ports) + self.mox.ReplayAll() + + ret_val = api.quantum.port_list(self.request) + for p in ret_val: + self.assertIsInstance(p, api.quantum.Port) + + def test_port_get(self): + port = {'port': self.api_ports.first()} + port_id = self.api_ports.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.show_port(port_id).AndReturn(port) + self.mox.ReplayAll() + + ret_val = api.quantum.port_get(self.request, port_id) + self.assertIsInstance(ret_val, api.quantum.Port) + + def test_port_create(self): + port_data = self.api_ports.first() + params = {'network_id': port_data['network_id'], + 'tenant_id': port_data['tenant_id'], + 'name': port_data['name'], + 'device_id': port_data['device_id']} + + quantumclient = self.stub_quantumclient() + quantumclient.create_port(body={'port': params})\ + .AndReturn({'port': port_data}) + self.mox.ReplayAll() + + ret_val = api.quantum.port_create(self.request, **params) + self.assertIsInstance(ret_val, api.quantum.Port) + self.assertEqual(ret_val.id, api.quantum.Port(port_data).id) + + def test_port_modify(self): + port_data = self.api_ports.first() + port_id = port_data['id'] + params = {'name': port_data['name'], + 'device_id': port_data['device_id']} + + quantumclient = self.stub_quantumclient() + quantumclient.update_port(port_id, body={'port': params})\ + .AndReturn({'port': port_data}) + self.mox.ReplayAll() + + ret_val = api.quantum.port_modify(self.request, port_id, **params) + self.assertIsInstance(ret_val, api.quantum.Port) + self.assertEqual(ret_val.id, api.quantum.Port(port_data).id) + + def test_port_delete(self): + port_id = self.api_ports.first()['id'] + + quantumclient = self.stub_quantumclient() + quantumclient.delete_port(port_id) + self.mox.ReplayAll() + + api.quantum.port_delete(self.request, port_id) diff --git a/horizon/tests/test_data/exceptions.py b/horizon/tests/test_data/exceptions.py index a33978faff..ab55941bcd 100644 --- a/horizon/tests/test_data/exceptions.py +++ b/horizon/tests/test_data/exceptions.py @@ -15,6 +15,7 @@ import glanceclient.exc as glance_exceptions from keystoneclient import exceptions as keystone_exceptions from novaclient import exceptions as nova_exceptions +from quantumclient.common import exceptions as quantum_exceptions from .utils import TestDataContainer @@ -53,3 +54,6 @@ def data(TEST): glance_exception = glance_exceptions.ClientException TEST.exceptions.glance = create_stubbed_exception(glance_exception) + + quantum_exception = quantum_exceptions.QuantumClientException + TEST.exceptions.quantum = create_stubbed_exception(quantum_exception) diff --git a/horizon/tests/test_data/quantum_data.py b/horizon/tests/test_data/quantum_data.py new file mode 100644 index 0000000000..b229e274fd --- /dev/null +++ b/horizon/tests/test_data/quantum_data.py @@ -0,0 +1,109 @@ +# Copyright 2012 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 copy + +from horizon.api.quantum import Network, Subnet, Port + +from .utils import TestDataContainer + + +def data(TEST): + # data returned by horizon.api.quantum wrapper + TEST.networks = TestDataContainer() + TEST.subnets = TestDataContainer() + TEST.ports = TestDataContainer() + + # data return by quantumclient + TEST.api_networks = TestDataContainer() + TEST.api_subnets = TestDataContainer() + TEST.api_ports = TestDataContainer() + + # 1st network + network_dict = {'admin_state_up': True, + 'id': '82288d84-e0a5-42ac-95be-e6af08727e42', + 'name': 'net1', + 'status': 'ACTIVE', + 'subnets': ['e8abc972-eb0c-41f1-9edd-4bc6e3bcd8c9'], + 'tenant_id': '1'} + subnet_dict = {'allocation_pools': [{'end': '10.0.0.254', + 'start': '10.0.0.2'}], + 'cidr': '10.0.0.0/24', + 'enable_dhcp': True, + 'gateway_ip': '10.0.0.1', + 'id': network_dict['subnets'][0], + 'ip_version': 4, + 'name': 'mysubnet1', + 'network_id': network_dict['id'], + 'tenant_id': network_dict['tenant_id']} + port_dict = {'admin_state_up': True, + 'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890', + 'fixed_ips': [{'ip_address': '10.0.0.3', + 'subnet_id': subnet_dict['id']}], + 'id': '3ec7f3db-cb2f-4a34-ab6b-69a64d3f008c', + 'mac_address': 'fa:16:3e:9c:d5:7e', + 'name': '', + 'network_id': network_dict['id'], + 'status': 'ACTIVE', + 'tenant_id': network_dict['tenant_id']} + + TEST.api_networks.add(network_dict) + TEST.api_subnets.add(subnet_dict) + TEST.api_ports.add(port_dict) + + network = copy.deepcopy(network_dict) + subnet = Subnet(subnet_dict) + network['subnets'] = [subnet] + TEST.networks.add(Network(network)) + TEST.subnets.add(subnet) + TEST.ports.add(Port(port_dict)) + + # 2nd network + network_dict = {'admin_state_up': True, + 'id': '72c3ab6c-c80f-4341-9dc5-210fa31ac6c2', + 'name': 'net2', + 'status': 'ACTIVE', + 'subnets': ['3f7c5d79-ee55-47b0-9213-8e669fb03009'], + 'tenant_id': '2'} + subnet_dict = {'allocation_pools': [{'end': '172.16.88.254', + 'start': '172.16.88.2'}], + 'cidr': '172.16.88.0/24', + 'enable_dhcp': True, + 'gateway_ip': '172.16.88.1', + 'id': '3f7c5d79-ee55-47b0-9213-8e669fb03009', + 'ip_version': 4, + 'name': 'aaaa', + 'network_id': network_dict['id'], + 'tenant_id': network_dict['tenant_id']} + port_dict = {'admin_state_up': True, + 'device_id': '40e536b1-b9fd-4eb7-82d6-84db5d65a2ac', + 'fixed_ips': [{'ip_address': '172.16.88.3', + 'subnet_id': subnet_dict['id']}], + 'id': '7e6ce62c-7ea2-44f8-b6b4-769af90a8406', + 'mac_address': 'fa:16:3e:56:e6:2f', + 'name': '', + 'network_id': network_dict['id'], + 'status': 'ACTIVE', + 'tenant_id': network_dict['tenant_id']} + + TEST.api_networks.add(network_dict) + TEST.api_subnets.add(subnet_dict) + TEST.api_ports.add(port_dict) + + network = copy.deepcopy(network_dict) + subnet = Subnet(subnet_dict) + network['subnets'] = [subnet] + TEST.networks.add(Network(network)) + TEST.subnets.add(subnet) + TEST.ports.add(Port(port_dict)) diff --git a/horizon/tests/test_data/utils.py b/horizon/tests/test_data/utils.py index b8a3f78602..28f6147f15 100644 --- a/horizon/tests/test_data/utils.py +++ b/horizon/tests/test_data/utils.py @@ -18,6 +18,7 @@ def load_test_data(load_onto=None): from . import glance_data from . import keystone_data from . import nova_data + from . import quantum_data from . import swift_data # The order of these loaders matters, some depend on others. @@ -25,6 +26,7 @@ def load_test_data(load_onto=None): keystone_data.data, glance_data.data, nova_data.data, + quantum_data.data, swift_data.data) if load_onto: for data_func in loaders: diff --git a/openstack_dashboard/exceptions.py b/openstack_dashboard/exceptions.py index 04b8ef2994..518c330d5a 100644 --- a/openstack_dashboard/exceptions.py +++ b/openstack_dashboard/exceptions.py @@ -22,6 +22,7 @@ from cloudfiles import errors as swiftclient from glanceclient.common import exceptions as glanceclient from keystoneclient import exceptions as keystoneclient from novaclient import exceptions as novaclient +from quantumclient.common import exceptions as quantumclient UNAUTHORIZED = (keystoneclient.Unauthorized, @@ -29,12 +30,16 @@ UNAUTHORIZED = (keystoneclient.Unauthorized, novaclient.Unauthorized, novaclient.Forbidden, glanceclient.Unauthorized, + quantumclient.Unauthorized, + quantumclient.Forbidden, swiftclient.AuthenticationFailed, swiftclient.AuthenticationError) NOT_FOUND = (keystoneclient.NotFound, novaclient.NotFound, glanceclient.NotFound, + quantumclient.NetworkNotFoundClient, + quantumclient.PortNotFoundClient, swiftclient.NoSuchContainer, swiftclient.NoSuchObject) @@ -44,4 +49,12 @@ RECOVERABLE = (keystoneclient.ClientException, keystoneclient.AuthorizationFailure, novaclient.ClientException, glanceclient.ClientException, + # NOTE(amotoki): Quantum exceptions other than the first one + # are recoverable in many cases (e.g., NetworkInUse is not + # raised once VMs which use the network are terminated). + quantumclient.QuantumClientException, + quantumclient.NetworkInUseClient, + quantumclient.PortInUseClient, + quantumclient.AlreadyAttachedClient, + quantumclient.StateInvalidClient, swiftclient.Error) diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index 72d543d490..aca092a88b 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -701,14 +701,14 @@ form.horizontal fieldset { .workflow td.actions { vertical-align: top; width: 308px; - padding-left: 10px; + padding-right: 10px; } .workflow td.help_text { vertical-align: top; width: 340px; - padding-right: 10px; - border-right: 1px solid #DDD; + padding-left: 10px; + border-left: 1px solid #DDD; } .workflow fieldset > table { diff --git a/tools/pip-requires b/tools/pip-requires index cb565cc1f5..8c6a82fd3d 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,6 +6,7 @@ python-cloudfiles python-glanceclient<2 python-keystoneclient python-novaclient +python-quantumclient pytz # Horizon Utility Requirements