diff --git a/openstack_dashboard/dashboards/admin/networks/ports/forms.py b/openstack_dashboard/dashboards/admin/networks/ports/forms.py index e6172ab07d..d60d1c9b0b 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/forms.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/forms.py @@ -59,11 +59,51 @@ class CreatePort(forms.SelfHandlingForm): "cases, different implementations can run on different " "hosts."), required=False) - + specify_ip = forms.ThemableChoiceField( + label=_("Specify IP address or subnet"), + help_text=_("To specify a subnet or a fixed IP, select any options."), + initial=False, + required=False, + choices=[('', _("Unspecified")), + ('subnet_id', _("Subnet")), + ('fixed_ip', _("Fixed IP Address"))], + widget=forms.Select(attrs={ + 'class': 'switchable', + 'data-slug': 'specify_ip', + })) + subnet_id = forms.ThemableChoiceField( + label=_("Subnet"), + required=False, + widget=forms.Select(attrs={ + 'class': 'switched', + 'data-switch-on': 'specify_ip', + 'data-specify_ip-subnet_id': _('Subnet'), + })) + fixed_ip = forms.IPField( + label=_("Fixed IP Address"), + required=False, + help_text=_("Specify the subnet IP address for the new port"), + version=forms.IPv4 | forms.IPv6, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'specify_ip', + 'data-specify_ip-fixed_ip': _('Fixed IP Address'), + })) failure_url = 'horizon:admin:networks:detail' def __init__(self, request, *args, **kwargs): super(CreatePort, self).__init__(request, *args, **kwargs) + + # prepare subnet choices and input area for each subnet + subnet_choices = self._get_subnet_choices(kwargs['initial']) + if subnet_choices: + subnet_choices.insert(0, ('', _("Select a subnet"))) + self.fields['subnet_id'].choices = subnet_choices + else: + self.fields['specify_ip'].widget = forms.HiddenInput() + self.fields['subnet_id'].widget = forms.HiddenInput() + self.fields['fixed_ip'].widget = forms.HiddenInput() + try: if api.neutron.is_extension_supported(request, 'binding'): neutron_settings = getattr(settings, @@ -100,20 +140,45 @@ class CreatePort(forms.SelfHandlingForm): msg = _("Unable to retrieve MAC learning state") exceptions.handle(self.request, msg) + def _get_subnet_choices(self, kwargs): + try: + network_id = kwargs['network_id'] + network = api.neutron.network_get(self.request, network_id) + except Exception: + return [] + + return [(subnet.id, '%s %s' % (subnet.name_or_id, subnet.cidr)) + for subnet in network.subnets] + 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.neutron.network_get(request, data['network_id']) - data['tenant_id'] = network.tenant_id - data['admin_state_up'] = (data['admin_state'] == 'True') - del data['network_name'] - del data['admin_state'] - if 'mac_state' in data: - data['mac_learning_enabled'] = data['mac_state'] - del data['mac_state'] + params = { + 'tenant_id': network.tenant_id, + 'network_id': data['network_id'], + 'admin_state_up': data['admin_state'] == 'True', + 'name': data['name'], + 'device_id': data['device_id'], + 'device_owner': data['device_owner'], + 'binding__host_id': data['binding__host_id'] + } - port = api.neutron.port_create(request, **data) + if data.get('specify_ip') == 'subnet_id': + if data.get('subnet_id'): + params['fixed_ips'] = [{"subnet_id": data['subnet_id']}] + elif data.get('specify_ip') == 'fixed_ip': + if data.get('fixed_ip'): + params['fixed_ips'] = [{"ip_address": data['fixed_ip']}] + + if data.get('binding__vnic_type'): + params['binding__vnic_type'] = data['binding__vnic_type'] + + if data.get('mac_state'): + params['mac_learning_enabled'] = data['mac_state'] + + port = api.neutron.port_create(request, **params) msg = _('Port %s was successfully created.') % port['id'] LOG.debug(msg) messages.success(request, msg) diff --git a/openstack_dashboard/dashboards/admin/networks/ports/tests.py b/openstack_dashboard/dashboards/admin/networks/ports/tests.py index 26e8c59eca..c4d5d650df 100644 --- a/openstack_dashboard/dashboards/admin/networks/ports/tests.py +++ b/openstack_dashboard/dashboards/admin/networks/ports/tests.py @@ -124,6 +124,9 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(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) @@ -167,6 +170,63 @@ class NetworkPortTests(test.BaseAdminViewTests): redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id]) self.assertRedirectsNoFollow(res, redir_url) + @test.create_stubs({api.neutron: ('network_get', + 'is_extension_supported', + 'port_create',)}) + def test_port_create_post_with_fixed_ip(self): + network = self.networks.first() + port = self.ports.first() + api.neutron.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(self.networks.first()) + api.neutron.network_get(IsA(http.HttpRequest), + network.id)\ + .AndReturn(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(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning')\ + .AndReturn(True) + extension_kwargs = {} + extension_kwargs['binding__vnic_type'] = \ + port.binding__vnic_type + api.neutron.port_create(IsA(http.HttpRequest), + tenant_id=network.tenant_id, + network_id=network.id, + name=port.name, + admin_state_up=port.admin_state_up, + device_id=port.device_id, + device_owner=port.device_owner, + binding__host_id=port.binding__host_id, + fixed_ips=port.fixed_ips, + **extension_kwargs)\ + .AndReturn(port) + self.mox.ReplayAll() + + form_data = {'network_id': port.network_id, + 'network_name': network.name, + 'name': port.name, + 'admin_state': port.admin_state_up, + 'device_id': port.device_id, + 'device_owner': port.device_owner, + 'binding__host_id': port.binding__host_id, + 'specify_ip': 'fixed_ip', + 'fixed_ip': port.fixed_ips[0]['ip_address'], + 'subnet_id': port.fixed_ips[0]['subnet_id']} + form_data['binding__vnic_type'] = port.binding__vnic_type + form_data['mac_state'] = True + url = reverse('horizon:admin:networks:addport', + args=[port.network_id]) + res = self.client.post(url, form_data) + + self.assertNoFormErrors(res) + redir_url = reverse(NETWORKS_DETAIL_URL, args=[port.network_id]) + self.assertRedirectsNoFollow(res, redir_url) + @test.create_stubs({api.neutron: ('network_get', 'port_create', 'is_extension_supported',)}) @@ -189,6 +249,9 @@ class NetworkPortTests(test.BaseAdminViewTests): api.neutron.network_get(IsA(http.HttpRequest), network.id)\ .AndReturn(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) diff --git a/releasenotes/notes/bug-1588663-6fab83e9d89b20d2.yaml b/releasenotes/notes/bug-1588663-6fab83e9d89b20d2.yaml new file mode 100644 index 0000000000..3737526cc8 --- /dev/null +++ b/releasenotes/notes/bug-1588663-6fab83e9d89b20d2.yaml @@ -0,0 +1,4 @@ +--- +features: + - Support a parameter to specify subnet or fixed IP address + when creating port.