diff --git a/openstack_dashboard/dashboards/project/instances/forms.py b/openstack_dashboard/dashboards/project/instances/forms.py index 1f65f0cd09..267b32f712 100644 --- a/openstack_dashboard/dashboards/project/instances/forms.py +++ b/openstack_dashboard/dashboards/project/instances/forms.py @@ -287,19 +287,78 @@ class DetachVolume(forms.SelfHandlingForm): class AttachInterface(forms.SelfHandlingForm): instance_id = forms.CharField(widget=forms.HiddenInput()) - network = forms.ThemableChoiceField(label=_("Network")) + specification_method = forms.ThemableChoiceField( + label=_("The way to specify an interface"), + initial=False, + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switchable', + 'data-slug': 'specification_method', + })) + port = forms.ThemableChoiceField( + label=_("Port"), + required=False, + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switched', + 'data-switch-on': 'specification_method', + 'data-specification_method-port': _('Port'), + })) + network = forms.ThemableChoiceField( + label=_("Network"), + required=False, + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switched', + 'data-switch-on': 'specification_method', + 'data-specification_method-network': _('Network'), + })) + fixed_ip = forms.IPField( + label=_("Fixed IP Address"), + required=False, + help_text=_("IP address for the new port"), + version=forms.IPv4 | forms.IPv6, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'specification_method', + 'data-specification_method-network': _('Fixed IP Address'), + })) def __init__(self, request, *args, **kwargs): super(AttachInterface, self).__init__(request, *args, **kwargs) networks = instance_utils.network_field_data(request, - include_empty_option=True) + include_empty_option=True, + with_cidr=True) self.fields['network'].choices = networks + choices = [('network', _("by Network (and IP address)"))] + ports = instance_utils.port_field_data(request, with_network=True) + if len(ports) > 0: + self.fields['port'].choices = ports + choices.append(('port', _("by Port"))) + + self.fields['specification_method'].choices = choices + + def clean_network(self): + specification_method = self.cleaned_data.get('specification_method') + network = self.cleaned_data.get('network') + if specification_method == 'network' and not network: + msg = _('This field is required.') + self._errors['network'] = self.error_class([msg]) + return network + def handle(self, request, data): instance_id = data['instance_id'] - network = data.get('network') try: - api.nova.interface_attach(request, instance_id, net_id=network) + net_id = port_id = fixed_ip = None + if data['specification_method'] == 'port': + port_id = data.get('port') + else: + net_id = data.get('network') + if data.get('fixed_ip'): + fixed_ip = data.get('fixed_ip') + api.nova.interface_attach(request, + instance_id, + net_id=net_id, + fixed_ip=fixed_ip, + port_id=port_id) msg = _('Attaching interface for instance %s.') % instance_id messages.success(request, msg) except Exception: diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 9b57ef6b1b..0d6f69b1c4 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -4423,7 +4423,9 @@ class ConsoleManagerTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron.network_list_for_tenant(IsA(http.HttpRequest), self.tenant.id) \ .AndReturn(self.networks.list()[:1]) - + api.neutron.network_list_for_tenant(IsA(http.HttpRequest), + self.tenant.id) \ + .AndReturn([]) self.mox.ReplayAll() url = reverse('horizon:project:instances:attach_interface', @@ -4436,17 +4438,23 @@ class ConsoleManagerTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.neutron: ('network_list_for_tenant',), api.nova: ('interface_attach',)}) def test_interface_attach_post(self): + fixed_ip = '10.0.0.10' server = self.servers.first() network = api.neutron.network_list_for_tenant(IsA(http.HttpRequest), self.tenant.id) \ .AndReturn(self.networks.list()[:1]) + api.neutron.network_list_for_tenant(IsA(http.HttpRequest), + self.tenant.id) \ + .AndReturn([]) api.nova.interface_attach(IsA(http.HttpRequest), server.id, - net_id=network[0].id) + net_id=network[0].id, fixed_ip=fixed_ip) self.mox.ReplayAll() form_data = {'instance_id': server.id, - 'network': network[0].id} + 'network': network[0].id, + 'specification_method': 'network', + 'fixed_ip': fixed_ip} url = reverse('horizon:project:instances:attach_interface', args=[server.id]) diff --git a/openstack_dashboard/dashboards/project/instances/utils.py b/openstack_dashboard/dashboards/project/instances/utils.py index 2e8690059a..a9a85b95c0 100644 --- a/openstack_dashboard/dashboards/project/instances/utils.py +++ b/openstack_dashboard/dashboards/project/instances/utils.py @@ -11,6 +11,7 @@ # under the License. import logging +from operator import itemgetter from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -84,7 +85,7 @@ def server_group_list(request): return [] -def network_field_data(request, include_empty_option=False): +def network_field_data(request, include_empty_option=False, with_cidr=False): """Returns a list of tuples of all networks. Generates a list of networks available to the user (request). And returns @@ -93,6 +94,7 @@ def network_field_data(request, include_empty_option=False): :param request: django http request object :param include_empty_option: flag to include a empty tuple in the front of the list + :param with_cidr: flag to include subnets cidr in field name :return: list of (id, name) tuples """ tenant_id = request.user.tenant_id @@ -100,12 +102,24 @@ def network_field_data(request, include_empty_option=False): if api.base.is_service_enabled(request, 'network'): try: networks = api.neutron.network_list_for_tenant(request, tenant_id) - networks = [(n.id, n.name_or_id) for n in networks if n['subnets']] - networks.sort(key=lambda obj: obj[1]) except Exception as e: msg = _('Failed to get network list {0}').format(six.text_type(e)) exceptions.handle(request, msg) + _networks = [] + for n in networks: + if not n['subnets']: + continue + v = n.name_or_id + if with_cidr: + cidrs = ([subnet.cidr for subnet in n['subnets'] + if subnet.ip_version == 4] + + [subnet.cidr for subnet in n['subnets'] + if subnet.ip_version == 6]) + v += ' (%s)' % ', '.join(cidrs) + _networks.append((n.id, v)) + networks = sorted(_networks, key=itemgetter(1)) + if not networks: if include_empty_option: return [("", _("No networks available")), ] @@ -167,21 +181,25 @@ def flavor_field_data(request, include_empty_option=False): return [] -def port_field_data(request): +def port_field_data(request, with_network=False): """Returns a list of tuples of all ports available for the tenant. Generates a list of ports that have no device_owner based on the networks available to the tenant doing the request. :param request: django http request object + :param with_network: include network name in field name :return: list of (id, name) tuples """ - def add_more_info_port_name(port): + def add_more_info_port_name(port, network): # add more info to the port for the display - return "{} ({})".format(port.name_or_id, - ",".join([ip['ip_address'] - for ip in port['fixed_ips']])) + port_name = "{} ({})".format( + port.name_or_id, ",".join( + [ip['ip_address'] for ip in port['fixed_ips']])) + if with_network and network: + port_name += " - {}".format(network.name_or_id) + return port_name ports = [] if api.base.is_service_enabled(request, 'network'): @@ -189,7 +207,7 @@ def port_field_data(request): request, request.user.tenant_id) for network in network_list: ports.extend( - [(port.id, add_more_info_port_name(port)) + [(port.id, add_more_info_port_name(port, network)) for port in api.neutron.port_list_with_trunk_types( request, network_id=network.id, tenant_id=request.user.tenant_id) diff --git a/releasenotes/notes/bug-1595913-5f0cd019b7c2173a.yaml b/releasenotes/notes/bug-1595913-5f0cd019b7c2173a.yaml new file mode 100644 index 0000000000..db9ee62f84 --- /dev/null +++ b/releasenotes/notes/bug-1595913-5f0cd019b7c2173a.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added the way to specify an interface when attaching it + to an instance. It can be specified by a network and a + fixed IP address (optional) or a port.