diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index f8c582f249..df63f8fd64 100755 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -678,6 +678,7 @@ Default:: 'enable_vpn': True, 'profile_support': None, 'supported_provider_types': ["*"], + 'supported_vnic_types': ["*"], 'segmentation_id_range': {} } @@ -803,6 +804,19 @@ be available to choose from. Example: ``['local', 'flat', 'gre']`` +``supported_vnic_types``: + +.. versionadded:: 2015.1(Kilo) + +Default ``['*']`` + +For use with the port binding extension. Use this to explicitly set which VNIC +types are supported; only those listed will be shown when creating or editing +a port. VNIC types include normal, direct and macvtap. By default all VNIC +types will be available to choose from. + +Example ``['normal', 'direct']`` + ``segmentation_id_range``: .. versionadded:: 2014.2(Juno) diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index 7035a68dfa..2ff31e7caf 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -88,9 +88,9 @@ class Network(NeutronAPIDictWrapper): def __init__(self, apiresource): apiresource['admin_state'] = \ 'UP' if apiresource['admin_state_up'] else 'DOWN' - # Django cannot handle a key name with a colon, so remap another key + # Django cannot handle a key name with ':', so use '__' for key in apiresource.keys(): - if key.find(':'): + if ':' in key: apiresource['__'.join(key.split(':'))] = apiresource[key] super(Network, self).__init__(apiresource) @@ -112,6 +112,10 @@ class Port(NeutronAPIDictWrapper): """Wrapper for neutron ports.""" def __init__(self, apiresource): + # Django cannot handle a key name with ':', so use '__' + for key in apiresource.keys(): + if ':' in key: + apiresource['__'.join(key.split(':'))] = apiresource[key] apiresource['admin_state'] = \ 'UP' if apiresource['admin_state_up'] else 'DOWN' if 'mac_learning_enabled' in apiresource: @@ -729,6 +733,13 @@ def port_get(request, port_id, **params): return Port(port) +def unescape_port_kwargs(**kwargs): + for key in kwargs: + if '__' in key: + kwargs[':'.join(key.split('__'))] = kwargs.pop(key) + return kwargs + + def port_create(request, network_id, **kwargs): """Create a port on a specified network. @@ -743,6 +754,7 @@ def port_create(request, network_id, **kwargs): # In the case policy profiles are being used, profile id is needed. if 'policy_profile_id' in kwargs: kwargs['n1kv:profile_id'] = kwargs.pop('policy_profile_id') + kwargs = unescape_port_kwargs(**kwargs) body = {'port': {'network_id': network_id}} if 'tenant_id' not in kwargs: kwargs['tenant_id'] = request.user.project_id @@ -758,6 +770,7 @@ def port_delete(request, port_id): def port_update(request, port_id, **kwargs): LOG.debug("port_update(): portid=%s, kwargs=%s" % (port_id, kwargs)) + kwargs = unescape_port_kwargs(**kwargs) body = {'port': kwargs} port = neutronclient(request).update_port(port_id, body=body).get('port') return Port(port) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/forms.py index 31950c1c6b..87dc14a23b 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/forms.py @@ -14,6 +14,7 @@ import logging +from django.conf import settings from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ @@ -27,6 +28,8 @@ from openstack_dashboard.dashboards.project.networks.ports \ LOG = logging.getLogger(__name__) +VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')), + ('macvtap', _('MacVTap'))] class CreatePort(forms.SelfHandlingForm): @@ -49,9 +52,36 @@ class CreatePort(forms.SelfHandlingForm): help_text=_("Device owner attached to the " "port"), required=False) + binding__host_id = forms.CharField( + label=_("Binding: Host"), + help_text=_("The ID of the host where the port is allocated. In some " + "cases, different implementations can run on different " + "hosts."), + required=False) + + failure_url = 'horizon:admin:networks:detail' def __init__(self, request, *args, **kwargs): super(CreatePort, self).__init__(request, *args, **kwargs) + if api.neutron.is_extension_supported(request, 'binding'): + neutron_settings = getattr(settings, + 'OPENSTACK_NEUTRON_NETWORK', {}) + supported_vnic_types = neutron_settings.get( + 'supported_vnic_types', ['*']) + if supported_vnic_types == ['*']: + vnic_type_choices = VNIC_TYPES + else: + vnic_type_choices = [ + vnic_type for vnic_type in VNIC_TYPES + if vnic_type[0] in supported_vnic_types + ] + + self.fields['binding__vnic_type'] = forms.ChoiceField( + choices=vnic_type_choices, + label=_("Binding: VNIC Type"), + help_text=_("The VNIC type that is bound to the neutron port"), + required=False) + if api.neutron.is_extension_supported(request, 'mac-learning'): self.fields['mac_state'] = forms.BooleanField( label=_("MAC Learning State"), initial=False, required=False) @@ -78,7 +108,7 @@ class CreatePort(forms.SelfHandlingForm): msg = _('Failed to create a port for network %s') \ % data['network_id'] LOG.info(msg) - redirect = reverse('horizon:admin:networks:detail', + redirect = reverse(self.failure_url, args=(data['network_id'],)) exceptions.handle(request, msg, redirect=redirect) @@ -92,6 +122,13 @@ class UpdatePort(project_forms.UpdatePort): help_text=_("Device owner attached to the " "port"), required=False) + binding__host_id = forms.CharField( + label=_("Binding: Host"), + help_text=_("The ID of the host where the port is allocated. In some " + "cases, different implementations can run on different " + "hosts."), + required=False) + failure_url = 'horizon:admin:networks:detail' def handle(self, request, data): @@ -99,13 +136,21 @@ class UpdatePort(project_forms.UpdatePort): LOG.debug('params = %s' % data) extension_kwargs = {} data['admin_state'] = (data['admin_state'] == 'True') + if 'binding__vnic_type' in data: + extension_kwargs['binding__vnic_type'] = \ + data['binding__vnic_type'] + if 'mac_state' in data: extension_kwargs['mac_learning_enabled'] = data['mac_state'] - port = api.neutron.port_update(request, data['port_id'], + + port = api.neutron.port_update(request, + data['port_id'], name=data['name'], admin_state_up=data['admin_state'], device_id=data['device_id'], device_owner=data['device_owner'], + binding__host_id=data + ['binding__host_id'], **extension_kwargs) msg = _('Port %s was successfully updated.') % data['port_id'] LOG.debug(msg) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tables.py b/openstack_dashboard/dashboards/admin/networks/ports/tables.py index 968bd30092..0bf8b5a0fb 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tables.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tables.py @@ -73,36 +73,14 @@ class CreatePort(tables.LinkAction): return reverse(self.url, args=(network_id,)) -class UpdatePort(policy.PolicyTargetMixin, tables.LinkAction): - name = "update" - verbose_name = _("Edit Port") +class UpdatePort(project_tables.UpdatePort): url = "horizon:admin:networks:editport" - classes = ("ajax-modal",) - icon = "pencil" - policy_rules = (("network", "update_port"),) - - 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): +class PortsTable(project_tables.PortsTable): name = tables.Column("name_or_id", verbose_name=_("Name"), link="horizon:admin:networks:ports:detail") - fixed_ips = tables.Column( - project_tables.get_fixed_ips, verbose_name=_("Fixed IPs")) - device_id = tables.Column( - project_tables.get_attached, verbose_name=_("Device Attached")) - status = tables.Column( - "status", - verbose_name=_("Status"), - display_choices=project_tables.STATUS_DISPLAY_CHOICES) - admin_state = tables.Column("admin_state", - verbose_name=_("Admin State"), - display_choices=project_tables.DISPLAY_CHOICES) - mac_state = tables.Column("mac_state", empty_value=api.neutron.OFF_STATE, - verbose_name=_("Mac Learning State")) class Meta(object): name = "ports" @@ -110,10 +88,3 @@ class PortsTable(tables.DataTable): table_actions = (CreatePort, DeletePort) row_actions = (UpdatePort, DeletePort,) hidden_title = False - - def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): - super(PortsTable, self).__init__(request, data=data, - needs_form_wrapper=needs_form_wrapper, - **kwargs) - if not api.neutron.is_extension_supported(request, 'mac-learning'): - del self.columns['mac_state'] diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tabs.py b/openstack_dashboard/dashboards/admin/networks/ports/tabs.py index 9129be0bba..042f49473a 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tabs.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tabs.py @@ -12,31 +12,13 @@ # 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 exceptions -from horizon import tabs - -from openstack_dashboard import api +from openstack_dashboard.dashboards.project.networks.ports \ + import tabs as project_tabs -class OverviewTab(tabs.Tab): - name = _("Overview") - slug = "overview" - template_name = "project/networks/ports/_detail_overview.html" - - def get_context_data(self, request): - port_id = self.tab_group.kwargs['port_id'] - try: - port = api.neutron.port_get(self.request, port_id) - except Exception: - redirect = reverse('horizon:admin:networks:index') - msg = _('Unable to retrieve port details.') - exceptions.handle(request, msg, redirect=redirect) - return {'port': port} +class OverviewTab(project_tabs.OverviewTab): + template_name = "admin/networks/ports/_detail_overview.html" -class PortDetailTabs(tabs.TabGroup): - slug = "port_details" +class PortDetailTabs(project_tabs.PortDetailTabs): tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/urls.py b/openstack_dashboard/dashboards/admin/networks/ports/urls.py index 9bed401980..02e92eb735 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/urls.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/urls.py @@ -15,7 +15,7 @@ from django.conf.urls import patterns from django.conf.urls import url -from openstack_dashboard.dashboards.project.networks.ports import views +from openstack_dashboard.dashboards.admin.networks.ports import views PORTS = r'^(?P[^/]+)/%s$' VIEW_MOD = 'openstack_dashboard.dashboards.admin.networks.ports.views' diff --git a/openstack_dashboard/dashboards/admin/networks/ports/views.py b/openstack_dashboard/dashboards/admin/networks/ports/views.py index f9cd2e0db8..2228bc878c 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/views.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/views.py @@ -20,26 +20,29 @@ from horizon import forms from horizon.utils import memoized from openstack_dashboard import api + +from openstack_dashboard.dashboards.admin.networks.ports \ + import forms as ports_forms +from openstack_dashboard.dashboards.admin.networks.ports \ + import tables as ports_tables +from openstack_dashboard.dashboards.admin.networks.ports \ + import tabs as ports_tabs from openstack_dashboard.dashboards.project.networks.ports \ import views as project_views -from openstack_dashboard.dashboards.admin.networks.ports \ - import forms as project_forms - class CreateView(forms.ModalFormView): - form_class = project_forms.CreatePort + form_class = ports_forms.CreatePort form_id = "create_port_form" modal_header = _("Create Port") - template_name = 'admin/networks/ports/create.html' submit_label = _("Create Port") submit_url = "horizon:admin:networks:addport" - success_url = 'horizon:admin:networks:detail' - failure_url = 'horizon:admin:networks:detail' page_title = _("Create Port") + template_name = 'admin/networks/ports/create.html' + url = 'horizon:admin:networks:detail' def get_success_url(self): - return reverse(self.success_url, + return reverse(self.url, args=(self.kwargs['network_id'],)) @memoized.memoized_method @@ -48,7 +51,7 @@ class CreateView(forms.ModalFormView): network_id = self.kwargs["network_id"] return api.neutron.network_get(self.request, network_id) except Exception: - redirect = reverse(self.failure_url, + redirect = reverse(self.url, args=(self.kwargs['network_id'],)) msg = _("Unable to retrieve network.") exceptions.handle(self.request, msg, redirect=redirect) @@ -66,9 +69,32 @@ class CreateView(forms.ModalFormView): "network_name": network.name} +class DetailView(project_views.DetailView): + tab_group_class = ports_tabs.PortDetailTabs + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + port = context["port"] + table = ports_tables.PortsTable(self.request, + network_id=port.network_id) + context["url"] = reverse('horizon:admin:networks:index') + context["actions"] = table.render_row_actions(port) + return context + + @staticmethod + def get_redirect_url(): + return reverse('horizon:admin:networks:index') + + class UpdateView(project_views.UpdateView): - form_class = project_forms.UpdatePort + form_class = ports_forms.UpdatePort template_name = 'admin/networks/ports/update.html' context_object_name = 'port' submit_url = "horizon:admin:networks:editport" success_url = 'horizon:admin:networks:detail' + + def get_initial(self): + initial = super(UpdateView, self).get_initial() + port = self._get_object() + initial['binding__host_id'] = port['binding__host_id'] + return initial diff --git a/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_detail_overview.html b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_detail_overview.html new file mode 100644 index 0000000000..ee296f4d3c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_detail_overview.html @@ -0,0 +1,73 @@ +{% load i18n sizeformat %} +{% load url from future %} + +
+
+
{% trans "Name" %}
+
{{ port.name|default:_("None") }}
+
{% trans "ID" %}
+
{{ port.id|default:_("None") }}
+ {% url 'horizon:project:networks:detail' port.network_id as network_url %} +
{% trans "Network ID" %}
+
{{ port.network_id|default:_("None") }}
+
{% trans "Project ID" %}
+
{{ port.tenant_id|default:_("-") }}
+
{% trans "MAC Address" %}
+
{{ port.mac_address|default:_("None") }}
+
{% trans "Status" %}
+
{{ port.status_label|default:_("None") }}
+
{% trans "Admin State" %}
+
{{ port.admin_state_label|default:_("None") }}
+ {% if port.mac_state %} +
{% trans "MAC Learning State" %}
+
{% trans "On" %}
+ {% endif %} +

{% trans "Fixed IP" %}

+
+ {% if port.fixed_ips.items|length > 1 %} + {% for ip in port.fixed_ips %} +
{% trans "IP Address" %}
+
{{ ip.ip_address }}
+ {% url 'horizon:project:networks:subnets:detail' ip.subnet_id as subnet_url %} +
{% trans "Subnet ID" %}
+
{{ ip.subnet_id }}
+ {% endfor %} + {% else %} +
{% trans "None" %}
+ {% endif %} +

{% trans "Attached Device" %}

+
+ {% if port.device_id|length > 1 or port.device_owner %} +
{% trans "Device Owner" %}
+
{{ port.device_owner|default:_("None") }}
+
{% trans "Device ID" %}
+
{{ port.device_id|default:_("None") }}
+ {% else %} +
{% trans "No attached device" %}
+ {% endif %} +

{% trans "Binding" %}

+
+
{% trans "Host" %}
+
{{ port.binding__host_id|default:_("None") }}
+
{% trans "Profile" %}
+
{{ port.binding__profile|default:_("None") }}
+
{% trans "VIF Type" %}
+
{{ port.binding__vif_type|replace_underscores }}
+
{% trans "VIF Details" %}
+ {% if port.binding__vif_details.items %} +
+
    + {% for key,value in port.binding__vif_details.items %} +
  • {{ key }} {{ value }}
  • + {% endfor %} +
+
+ {% else %} +
{% trans "None" %}
+ {% endif %} + {% if port.binding__vnic_type %} +
{% trans "VNIC Type" %}
+
{{ port.binding__vnic_type }}
+ {% endif %} +
+
diff --git a/openstack_dashboard/dashboards/admin/networks/tests.py b/openstack_dashboard/dashboards/admin/networks/tests.py index e26ad31258..09979ba684 100644 --- a/openstack_dashboard/dashboards/admin/networks/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/tests.py @@ -975,7 +975,9 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) - + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning')\ + .AndReturn(mac_learning) self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:networks:ports:detail', @@ -997,7 +999,7 @@ class NetworkPortTests(test.BaseAdminViewTests): # admin DetailView is shared with userpanel one, so # redirection URL on error is userpanel index. - redir_url = reverse('horizon:project:networks:index') + redir_url = reverse('horizon:admin:networks:index') self.assertRedirectsNoFollow(res, redir_url) @test.create_stubs({api.neutron: ('network_get', @@ -1010,11 +1012,14 @@ class NetworkPortTests(test.BaseAdminViewTests): def test_port_create_get_with_mac_learning(self): self._test_port_create_get(mac_learning=True) - def _test_port_create_get(self, mac_learning=False): + def _test_port_create_get(self, mac_learning=False, binding=False): network = self.networks.first() api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) @@ -1036,9 +1041,9 @@ class NetworkPortTests(test.BaseAdminViewTests): 'is_extension_supported', 'port_create',)}) def test_port_create_post_with_mac_learning(self): - self._test_port_create_post(mac_learning=True) + self._test_port_create_post(mac_learning=True, binding=False) - def _test_port_create_post(self, mac_learning=False): + def _test_port_create_post(self, mac_learning=False, binding=False): network = self.networks.first() port = self.ports.first() api.neutron.network_get(IsA(http.HttpRequest), @@ -1047,10 +1052,16 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = \ + port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_create(IsA(http.HttpRequest), @@ -1060,6 +1071,7 @@ class NetworkPortTests(test.BaseAdminViewTests): admin_state_up=port.admin_state_up, device_id=port.device_id, device_owner=port.device_owner, + binding__host_id=port.binding__host_id, **extension_kwargs)\ .AndReturn(port) self.mox.ReplayAll() @@ -1069,7 +1081,10 @@ class NetworkPortTests(test.BaseAdminViewTests): 'name': port.name, 'admin_state': port.admin_state_up, 'device_id': port.device_id, - 'device_owner': port.device_owner} + 'device_owner': port.device_owner, + 'binding__host_id': port.binding__host_id} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_state'] = True url = reverse('horizon:admin:networks:addport', @@ -1093,7 +1108,8 @@ class NetworkPortTests(test.BaseAdminViewTests): def test_port_create_post_exception_with_mac_learning(self): self._test_port_create_post_exception(mac_learning=True) - def _test_port_create_post_exception(self, mac_learning=False): + def _test_port_create_post_exception(self, mac_learning=False, + binding=False): network = self.networks.first() port = self.ports.first() api.neutron.network_get(IsA(http.HttpRequest), @@ -1102,10 +1118,15 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(self.networks.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_create(IsA(http.HttpRequest), @@ -1115,6 +1136,7 @@ class NetworkPortTests(test.BaseAdminViewTests): admin_state_up=port.admin_state_up, device_id=port.device_id, device_owner=port.device_owner, + binding__host_id=port.binding__host_id, **extension_kwargs)\ .AndRaise(self.exceptions.neutron) self.mox.ReplayAll() @@ -1125,7 +1147,10 @@ class NetworkPortTests(test.BaseAdminViewTests): 'admin_state': port.admin_state_up, 'mac_state': True, 'device_id': port.device_id, - 'device_owner': port.device_owner} + 'device_owner': port.device_owner, + 'binding__host_id': port.binding__host_id} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_learning_enabled'] = True url = reverse('horizon:admin:networks:addport', @@ -1147,11 +1172,14 @@ class NetworkPortTests(test.BaseAdminViewTests): def test_port_update_get_with_mac_learning(self): self._test_port_update_get(mac_learning=True) - def _test_port_update_get(self, mac_learning=False): + def _test_port_update_get(self, mac_learning=False, binding=False): port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) @@ -1175,14 +1203,19 @@ class NetworkPortTests(test.BaseAdminViewTests): def test_port_update_post_with_mac_learning(self): self._test_port_update_post(mac_learning=True) - def _test_port_update_post(self, mac_learning=False): + def _test_port_update_post(self, mac_learning=False, binding=False): port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_update(IsA(http.HttpRequest), port.id, @@ -1190,6 +1223,7 @@ class NetworkPortTests(test.BaseAdminViewTests): admin_state_up=port.admin_state_up, device_id=port.device_id, device_owner=port.device_owner, + binding__host_id=port.binding__host_id, **extension_kwargs)\ .AndReturn(port) self.mox.ReplayAll() @@ -1199,7 +1233,10 @@ class NetworkPortTests(test.BaseAdminViewTests): 'name': port.name, 'admin_state': port.admin_state_up, 'device_id': port.device_id, - 'device_owner': port.device_owner} + 'device_owner': port.device_owner, + 'binding__host_id': port.binding__host_id} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_state'] = True url = reverse('horizon:admin:networks:editport', @@ -1220,16 +1257,22 @@ class NetworkPortTests(test.BaseAdminViewTests): 'is_extension_supported', 'port_update')}) def test_port_update_post_exception_with_mac_learning(self): - self._test_port_update_post_exception(mac_learning=True) + self._test_port_update_post_exception(mac_learning=True, binding=False) - def _test_port_update_post_exception(self, mac_learning=False): + def _test_port_update_post_exception(self, mac_learning=False, + binding=False): port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_update(IsA(http.HttpRequest), port.id, @@ -1237,6 +1280,7 @@ class NetworkPortTests(test.BaseAdminViewTests): admin_state_up=port.admin_state_up, device_id=port.device_id, device_owner=port.device_owner, + binding__host_id=port.binding__host_id, **extension_kwargs)\ .AndRaise(self.exceptions.neutron) self.mox.ReplayAll() @@ -1246,7 +1290,10 @@ class NetworkPortTests(test.BaseAdminViewTests): 'name': port.name, 'admin_state': port.admin_state_up, 'device_id': port.device_id, - 'device_owner': port.device_owner} + 'device_owner': port.device_owner, + 'binding__host_id': port.binding__host_id} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_state'] = True url = reverse('horizon:admin:networks:editport', diff --git a/openstack_dashboard/dashboards/project/networks/ports/forms.py b/openstack_dashboard/dashboards/project/networks/ports/forms.py index 967a700525..138585aec9 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/project/networks/ports/forms.py @@ -14,6 +14,7 @@ import logging +from django.conf import settings from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ @@ -25,6 +26,8 @@ from openstack_dashboard import api LOG = logging.getLogger(__name__) +VNIC_TYPES = [('normal', _('Normal')), ('direct', _('Direct')), + ('macvtap', _('MacVTap'))] class UpdatePort(forms.SelfHandlingForm): @@ -42,18 +45,42 @@ class UpdatePort(forms.SelfHandlingForm): def __init__(self, request, *args, **kwargs): super(UpdatePort, self).__init__(request, *args, **kwargs) + + if api.neutron.is_extension_supported(request, 'binding'): + neutron_settings = getattr(settings, + 'OPENSTACK_NEUTRON_NETWORK', {}) + supported_vnic_types = neutron_settings.get( + 'supported_vnic_types', ['*']) + if supported_vnic_types == ['*']: + vnic_type_choices = VNIC_TYPES + else: + vnic_type_choices = [ + vnic_type for vnic_type in VNIC_TYPES + if vnic_type[0] in supported_vnic_types + ] + + self.fields['binding__vnic_type'] = forms.ChoiceField( + choices=vnic_type_choices, + label=_("Binding: VNIC Type"), + help_text=_("The VNIC type that is bound to the neutron port"), + required=False) + if api.neutron.is_extension_supported(request, 'mac-learning'): self.fields['mac_state'] = forms.BooleanField( - label=_("Mac Learning State"), required=False) + label=_("MAC Learning State"), initial=False, required=False) def handle(self, request, data): data['admin_state'] = (data['admin_state'] == 'True') try: LOG.debug('params = %s' % data) extension_kwargs = {} + if 'binding__vnic_type' in data: + extension_kwargs['binding__vnic_type'] = \ + data['binding__vnic_type'] if 'mac_state' in data: extension_kwargs['mac_learning_enabled'] = data['mac_state'] - port = api.neutron.port_update(request, data['port_id'], + port = api.neutron.port_update(request, + data['port_id'], name=data['name'], admin_state_up=data['admin_state'], **extension_kwargs) diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py index 80881a24b6..b855a73cc3 100644 --- a/openstack_dashboard/dashboards/project/networks/ports/views.py +++ b/openstack_dashboard/dashboards/project/networks/ports/views.py @@ -100,8 +100,7 @@ class UpdateView(forms.ModalFormView): try: return api.neutron.port_get(self.request, port_id) except Exception: - redirect = reverse("horizon:project:networks:detail", - args=(self.kwargs['network_id'],)) + redirect = self.get_success_url() msg = _('Unable to retrieve port details') exceptions.handle(self.request, msg, redirect=redirect) @@ -120,9 +119,9 @@ class UpdateView(forms.ModalFormView): 'network_id': port['network_id'], 'tenant_id': port['tenant_id'], 'name': port['name'], - 'admin_state': port['admin_state_up'], - 'device_id': port['device_id'], - 'device_owner': port['device_owner']} + 'admin_state': port['admin_state_up']} + if port['binding__vnic_type']: + initial['binding__vnic_type'] = port['binding__vnic_type'] try: initial['mac_state'] = port['mac_learning_enabled'] except Exception: diff --git a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html index f9d9f86a10..f1e1f6e0ae 100644 --- a/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html @@ -1,11 +1,7 @@ {% load i18n sizeformat %} {% load url from future %} -

{% trans "Port Overview" %}

- -
-

{% trans "Port" %}

-
+
{% trans "Name" %}
{{ port.name|default:_("None") }}
@@ -16,18 +12,7 @@
{{ 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 %} - {% trans "None" %} - {% endif %} -
-
{% trans "Mac Address" %}
+
{% trans "MAC Address" %}
{{ port.mac_address|default:_("None") }}
{% trans "Status" %}
{{ port.status_label|default:_("None") }}
@@ -37,12 +22,34 @@
{% trans "MAC Learning State" %}
{{ port.mac_state }}
{% endif %} -
{% trans "Attached Device" %}
+

{% trans "Fixed IP" %}

+
+ {% if port.fixed_ips.items|length > 1 %} + {% for ip in port.fixed_ips %} +
{% trans "IP Address" %}
+
{{ ip.ip_address }}
+ {% url 'horizon:project:networks:subnets:detail' ip.subnet_id as subnet_url %} +
{% trans "Subnet ID" %}
+
{{ ip.subnet_id }}
+ {% endfor %} + {% else %} +
{% trans "None" %}
+ {% endif %} +

{% trans "Attached Device" %}

+
{% if port.device_id|length > 1 or port.device_owner %} -
{% trans "Device Owner" %}: {{ port.device_owner|default:_("None") }}
-
{% trans "Device ID" %}: {{ port.device_id|default:_("-") }}
+
{% trans "Device Owner" %}
+
{{ port.device_owner|default:_("None") }}
+
{% trans "Device ID" %}
+
{{ port.device_id|default:_("None") }}
{% else %}
{% trans "No attached device" %}
{% endif %} +

{% trans "Binding" %}

+
+ {% if port.binding__vnic_type %} +
{% trans "VNIC Type" %}
+
{{ port.binding__vnic_type }}
+ {% endif %}
diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py index a9d507aa8f..e39dab44e0 100644 --- a/openstack_dashboard/dashboards/project/networks/tests.py +++ b/openstack_dashboard/dashboards/project/networks/tests.py @@ -1691,11 +1691,14 @@ class NetworkPortTests(test.TestCase): def test_port_update_get_with_mac_learning(self): self._test_port_update_get(mac_learning=True) - def _test_port_update_get(self, mac_learning=False): + def _test_port_update_get(self, mac_learning=False, binding=False): port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) @@ -1719,14 +1722,19 @@ class NetworkPortTests(test.TestCase): def test_port_update_post_with_mac_learning(self): self._test_port_update_post(mac_learning=True) - def _test_port_update_post(self, mac_learning=False): + def _test_port_update_post(self, mac_learning=False, binding=False): port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_update(IsA(http.HttpRequest), port.id, @@ -1740,6 +1748,8 @@ class NetworkPortTests(test.TestCase): 'port_id': port.id, 'name': port.name, 'admin_state': port.admin_state_up} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_state'] = True url = reverse('horizon:project:networks:editport', @@ -1762,14 +1772,21 @@ class NetworkPortTests(test.TestCase): def test_port_update_post_exception_with_mac_learning(self): self._test_port_update_post_exception(mac_learning=True) - def _test_port_update_post_exception(self, mac_learning=False): + def _test_port_update_post_exception(self, mac_learning=False, + binding=False): + port = self.ports.first() api.neutron.port_get(IsA(http.HttpRequest), port.id)\ .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'binding')\ + .AndReturn(binding) api.neutron.is_extension_supported(IsA(http.HttpRequest), 'mac-learning')\ .AndReturn(mac_learning) extension_kwargs = {} + if binding: + extension_kwargs['binding__vnic_type'] = port.binding__vnic_type if mac_learning: extension_kwargs['mac_learning_enabled'] = True api.neutron.port_update(IsA(http.HttpRequest), port.id, @@ -1783,6 +1800,8 @@ class NetworkPortTests(test.TestCase): 'port_id': port.id, 'name': port.name, 'admin_state': port.admin_state_up} + if binding: + form_data['binding__vnic_type'] = port.binding__vnic_type if mac_learning: form_data['mac_state'] = True url = reverse('horizon:project:networks:editport', diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index acc4505346..9c2c12d7a7 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -216,6 +216,12 @@ OPENSTACK_NEUTRON_NETWORK = { # in this list will be available to choose from when creating a network. # Network types include local, flat, vlan, gre, and vxlan. 'supported_provider_types': ['*'], + + # Set which VNIC types are supported for port binding. Only the VNIC + # types in this list will be available to choose from when creating a + # port. + # VNIC types include 'normal', 'macvtap' and 'direct'. + 'supported_vnic_types': ['*'] } # The OPENSTACK_IMAGE_BACKEND settings can be used to customize features diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index 4c08448c73..c25e67b841 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -161,7 +161,10 @@ def data(TEST): 'name': '', 'network_id': network_dict['id'], 'status': 'ACTIVE', - 'tenant_id': network_dict['tenant_id']} + 'tenant_id': network_dict['tenant_id'], + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} + TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) @@ -175,7 +178,9 @@ def data(TEST): 'name': '', 'network_id': network_dict['id'], 'status': 'ACTIVE', - 'tenant_id': network_dict['tenant_id']} + 'tenant_id': network_dict['tenant_id'], + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) assoc_port = port_dict @@ -190,7 +195,9 @@ def data(TEST): 'name': '', 'network_id': network_dict['id'], 'status': 'ACTIVE', - 'tenant_id': network_dict['tenant_id']} + 'tenant_id': network_dict['tenant_id'], + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) @@ -238,7 +245,9 @@ def data(TEST): 'name': '', 'network_id': network_dict['id'], 'status': 'ACTIVE', - 'tenant_id': network_dict['tenant_id']} + 'tenant_id': network_dict['tenant_id'], + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) @@ -350,7 +359,9 @@ def data(TEST): 'name': '', 'network_id': TEST.networks.get(name="ext_net")['id'], 'status': 'ACTIVE', - 'tenant_id': '1'} + 'tenant_id': '1', + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict)) @@ -1142,6 +1153,8 @@ def data(TEST): 'name': 'port5', 'network_id': TEST.networks.get(name="net4")['id'], 'status': 'ACTIVE', - 'tenant_id': TEST.networks.get(name="net4")['tenant_id']} + 'tenant_id': TEST.networks.get(name="net4")['tenant_id'], + 'binding:vnic_type': 'normal', + 'binding:host_id': 'host'} TEST.api_ports.add(port_dict) TEST.ports.add(neutron.Port(port_dict))