Add Network Port selection to instance launch

Adds a new step to the launch instance wizard
to select any available ports to attach on launch.
Modifies the existing tests to take the new step
and calls insto consideration.

DocImpact: Add port info to user-guide/dashboard_launch_instances.html
Change-Id: I97b24be0d75b69638aeb52bda7d0d0541a80663a
Implements: blueprint allow-launching-ports
This commit is contained in:
Itxaka 2016-01-22 00:33:13 +01:00 committed by Itxaka Serrano Garcia
parent ba2bc3503e
commit c0aab9adf3
5 changed files with 333 additions and 22 deletions

View File

@ -0,0 +1,7 @@
{% load i18n %}
{% block help_message %}
<p>{% 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 %}</p>
<p>{% blocktrans %}The port also describes the associated network configuration, such as the MAC and IP addresses to be used on that port.{% endblocktrans %}</p>
<p>{% 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 %}</p>
{% endblock %}

View File

@ -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):
['<SetInstanceDetails: setinstancedetailsaction>',
'<SetAccessControls: setaccesscontrolsaction>',
'<SetNetwork: setnetworkaction>',
'<SetNetworkPorts: setnetworkportsaction>',
'<PostCreationStep: customizeaction>',
'<SetAdvanced: setadvancedaction>'])
@ -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()

View File

@ -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

View File

@ -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'],

View File

@ -0,0 +1,3 @@
---
features:
- Allows to attach ports during instance launch <https://blueprints.launchpad.net/horizon/+spec/allow-launching-ports>