diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_network_ports_help.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_network_ports_help.html
new file mode 100644
index 000000000..7f91c2054
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_network_ports_help.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+
+{% block help_message %}
+
{% blocktrans %}A port is a connection point for attaching a single device, such as the NIC of a virtual server, to a virtual network.{% endblocktrans %}
+{% blocktrans %}The port also describes the associated network configuration, such as the MAC and IP addresses to be used on that port.{% endblocktrans %}
+{% blocktrans %}Ports are optional and can be used with networks to add extra IP addresses to your instances or select specific types of ports.{% endblocktrans %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 48aa7541a..55809b0f7 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -1468,7 +1468,8 @@ class InstanceTests(helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.glance: ('image_list_detailed',)})
def test_launch_instance_get(self,
expect_password_fields=True,
@@ -1509,6 +1510,19 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
+
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -1549,6 +1563,7 @@ class InstanceTests(helpers.TestCase):
['',
'',
'',
+ '',
'',
''])
@@ -1693,7 +1708,8 @@ class InstanceTests(helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.glance: ('image_list_detailed',)})
def test_launch_instance_get_bootable_volumes(self,
block_device_mapping_v2=True,
@@ -1732,7 +1748,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
-
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -1785,7 +1811,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
- 'port_create',),
+ 'port_create',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -1837,6 +1864,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
policy_profile_id = self.policy_profiles.first().id
@@ -1985,7 +2023,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
-
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
policy_profiles = self.policy_profiles.list()
policy_profile_id = self.policy_profiles.first().id
port_one = self.ports.first()
@@ -2060,7 +2108,8 @@ class InstanceTests(helpers.TestCase):
api.neutron: ('network_list',
'profile_list',
'port_create',
- 'port_delete',),
+ 'port_delete',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2078,7 +2127,8 @@ class InstanceTests(helpers.TestCase):
api.neutron: ('network_list',
'profile_list',
'port_create',
- 'port_delete',),
+ 'port_delete',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2094,7 +2144,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
- 'port_create',),
+ 'port_create',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2165,6 +2216,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
if test_with_profile:
@@ -2253,7 +2315,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
- 'port_create'),
+ 'port_create',
+ 'port_list'),
api.nova: ('server_create',
'extension_supported',
'flavor_list',
@@ -2308,6 +2371,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
if test_with_profile:
@@ -2393,7 +2467,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2436,6 +2511,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
if test_with_profile:
@@ -2495,7 +2581,8 @@ class InstanceTests(helpers.TestCase):
api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
- 'port_create',),
+ 'port_create',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2567,6 +2654,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
if test_with_profile:
@@ -2657,7 +2755,8 @@ class InstanceTests(helpers.TestCase):
api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
- 'port_create',),
+ 'port_create',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2698,6 +2797,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
api.nova.keypair_list(IsA(http.HttpRequest)) \
@@ -2749,7 +2859,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
cinder: ('volume_list',
'volume_snapshot_list',),
api.network: ('security_group_list',),
@@ -2785,6 +2896,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -2823,7 +2945,8 @@ class InstanceTests(helpers.TestCase):
api.neutron: ('network_list',
'profile_list',
'port_create',
- 'port_delete'),
+ 'port_delete',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -2880,6 +3003,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
policy_profile_id = self.policy_profiles.first().id
@@ -2956,7 +3090,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3007,6 +3142,18 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -3058,7 +3205,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3114,6 +3262,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -3189,7 +3348,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3239,6 +3399,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -3330,7 +3501,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3380,6 +3552,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(
IsA(http.HttpRequest),
shared=True).AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.extension_supported(
'DiskConfig', IsA(http.HttpRequest)).AndReturn(True)
api.nova.extension_supported(
@@ -3449,7 +3632,8 @@ class InstanceTests(helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
- 'profile_list',),
+ 'profile_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3502,6 +3686,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -3681,7 +3876,8 @@ class InstanceTests(helpers.TestCase):
six.text_type(launch_action.verbose_name))
@helpers.create_stubs({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',),
+ api.neutron: ('network_list',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -3739,6 +3935,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
api.nova.extension_supported('DiskConfig',
IsA(http.HttpRequest)) \
.AndReturn(True)
@@ -3845,7 +4052,8 @@ class InstanceTests(helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
- 'profile_list'),
+ 'profile_list',
+ 'port_list'),
api.glance: ('image_list_detailed',)})
def test_select_default_keypair_if_only_one(self,
test_with_profile=False):
@@ -3873,6 +4081,17 @@ class InstanceTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
if test_with_profile:
policy_profiles = self.policy_profiles.list()
api.neutron.profile_list(IsA(http.HttpRequest),
@@ -4846,7 +5065,8 @@ class ConsoleManagerTests(helpers.TestCase):
api.neutron: ('network_list',
'profile_list',
'port_create',
- 'port_delete'),
+ 'port_delete',
+ 'port_list'),
api.nova: ('extension_supported',
'flavor_list',
'keypair_list',
@@ -4901,6 +5121,17 @@ class ConsoleManagerTests(helpers.TestCase):
api.neutron.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ shared=False) \
+ .AndReturn(self.networks.list()[:1])
+ api.neutron.network_list(IsA(http.HttpRequest),
+ shared=True) \
+ .AndReturn(self.networks.list()[1:])
+ for net in self.networks.list():
+ api.neutron.port_list(IsA(http.HttpRequest),
+ network_id=net.id) \
+ .AndReturn(self.ports.list())
policy_profiles = self.policy_profiles.list()
policy_profile_id = self.policy_profiles.first().id
port = self.ports.first()
diff --git a/openstack_dashboard/dashboards/project/instances/utils.py b/openstack_dashboard/dashboards/project/instances/utils.py
index 81458ea1c..e85ae80ab 100644
--- a/openstack_dashboard/dashboards/project/instances/utils.py
+++ b/openstack_dashboard/dashboards/project/instances/utils.py
@@ -155,3 +155,33 @@ def flavor_field_data(request, include_empty_option=False):
if include_empty_option:
return [("", _("No flavors available")), ]
return []
+
+
+def port_field_data(request):
+ """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
+ :return: list of (id, name) tuples
+ """
+
+ def add_more_info_port_name(port):
+ # 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']]))
+
+ ports = []
+ if api.base.is_service_enabled(request, 'network'):
+ network_list = api.neutron.network_list_for_tenant(
+ request, request.user.tenant_id)
+ for network in network_list:
+ ports.extend(
+ [(port.id, add_more_info_port_name(port))
+ for port in api.neutron.port_list(request,
+ network_id=network.id)
+ if port.device_owner == ''])
+ ports.sort(key=lambda obj: obj[1])
+ return ports
diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py
index f2b7698e3..00e7481a7 100644
--- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py
+++ b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py
@@ -779,6 +779,39 @@ class SetNetwork(workflows.Step):
return context
+class SetNetworkPortsAction(workflows.Action):
+ ports = forms.MultipleChoiceField(label=_("Ports"),
+ widget=forms.CheckboxSelectMultiple(),
+ required=False,
+ help_text=_("Launch instance with"
+ " these ports"))
+
+ class Meta(object):
+ name = _("Network Ports")
+ permissions = ('openstack.services.network',)
+ help_text_template = ("project/instances/"
+ "_launch_network_ports_help.html")
+
+ def populate_ports_choices(self, request, context):
+ ports = instance_utils.port_field_data(request)
+ if not ports:
+ self.fields['ports'].label = _("No ports available")
+ self.fields['ports'].help_text = _("No ports available")
+ return ports
+
+
+class SetNetworkPorts(workflows.Step):
+ action_class = SetNetworkPortsAction
+ contributes = ("ports",)
+
+ def contribute(self, data, context):
+ if data:
+ ports = self.workflow.request.POST.getlist("ports")
+ if ports:
+ context['ports'] = ports
+ return context
+
+
class SetAdvancedAction(workflows.Action):
disk_config = forms.ChoiceField(
label=_("Disk Partition"), required=False,
@@ -844,6 +877,7 @@ class LaunchInstance(workflows.Workflow):
SetInstanceDetails,
SetAccessControls,
SetNetwork,
+ SetNetworkPorts,
PostCreationStep,
SetAdvanced)
@@ -932,6 +966,12 @@ class LaunchInstance(workflows.Workflow):
context['network_id'],
context['profile_id'])
+ ports = context.get('ports')
+ if ports:
+ if nics is None:
+ nics = []
+ nics.extend([{'port-id': port} for port in ports])
+
try:
api.nova.server_create(request,
context['name'],
diff --git a/releasenotes/notes/bp-allow-launching-ports-b1fcc495777b7f4c.yaml b/releasenotes/notes/bp-allow-launching-ports-b1fcc495777b7f4c.yaml
new file mode 100644
index 000000000..af5741f47
--- /dev/null
+++ b/releasenotes/notes/bp-allow-launching-ports-b1fcc495777b7f4c.yaml
@@ -0,0 +1,3 @@
+---
+features:
+ - Allows to attach ports during instance launch