Merge "The Docker "expose" option allows that a port-range and protocol be specified through the "docker run" command: docker run --net=my_kuryr_net --expose=1234-1238/udp -it ubuntu This patch set implements this feature by using Neutron security groups."
This commit is contained in:
commit
1ffffcc401
@ -21,26 +21,33 @@ SCHEMA = {
|
||||
# Routes are either given a RouteType of 0 and a value for NextHop;
|
||||
# or, a RouteType of 1 and no value for NextHop, meaning a connected route.
|
||||
ROUTE_TYPE = {
|
||||
"NEXTHOP": 0,
|
||||
"CONNECTED": 1
|
||||
"NEXTHOP": 0,
|
||||
"CONNECTED": 1
|
||||
}
|
||||
|
||||
PROTOCOLS = {
|
||||
1: 'icmp',
|
||||
6: 'tcp',
|
||||
17: 'udp'
|
||||
}
|
||||
|
||||
PORT_STATUS_ACTIVE = 'ACTIVE'
|
||||
PORT_STATUS_DOWN = 'DOWN'
|
||||
|
||||
CONTAINER_VETH_PREFIX = 't_c'
|
||||
DEVICE_OWNER = 'kuryr:container'
|
||||
NIC_NAME_LEN = 14
|
||||
VETH_PREFIX = 'tap'
|
||||
CONTAINER_VETH_PREFIX = 't_c'
|
||||
|
||||
NET_NAME_PREFIX = 'kuryr-net-'
|
||||
NEUTRON_ID_LH_OPTION = 'kuryr.net.uuid.lh'
|
||||
NEUTRON_ID_UH_OPTION = 'kuryr.net.uuid.uh'
|
||||
NET_NAME_PREFIX = 'kuryr-net-'
|
||||
|
||||
REQUEST_ADDRESS_TYPE = 'RequestAddressType'
|
||||
DOCKER_EXPOSED_PORTS_OPTION = 'com.docker.network.endpoint.exposedports'
|
||||
KURYR_EXISTING_NEUTRON_NET = 'kuryr.net.existing'
|
||||
NETWORK_GATEWAY_OPTIONS = 'com.docker.network.gateway'
|
||||
NETWORK_GENERIC_OPTIONS = 'com.docker.network.generic'
|
||||
NEUTRON_UUID_OPTION = 'neutron.net.uuid'
|
||||
NEUTRON_NAME_OPTION = 'neutron.net.name'
|
||||
KURYR_EXISTING_NEUTRON_NET = 'kuryr.net.existing'
|
||||
NEUTRON_POOL_NAME_OPTION = 'neutron.pool.name'
|
||||
NEUTRON_UUID_OPTION = 'neutron.net.uuid'
|
||||
REQUEST_ADDRESS_TYPE = 'RequestAddressType'
|
||||
|
@ -217,6 +217,14 @@ def _get_subnets_by_interface_cidr(neutron_network_id,
|
||||
return subnets
|
||||
|
||||
|
||||
def _get_neutron_port_from_docker_endpoint(endpoint_id):
|
||||
port_name = utils.get_neutron_port_name(endpoint_id)
|
||||
filtered_ports = app.neutron.list_ports(name=port_name)
|
||||
num_ports = len(filtered_ports.get('ports', []))
|
||||
if num_ports == 1:
|
||||
return filtered_ports['ports'][0]['id']
|
||||
|
||||
|
||||
def _process_interface_address(port_dict, subnets_dict_by_id,
|
||||
response_interface):
|
||||
assigned_address = port_dict['ip_address']
|
||||
@ -394,6 +402,109 @@ def _port_active(neutron_port_id, vif_plug_timeout):
|
||||
return port_active
|
||||
|
||||
|
||||
def _program_expose_ports(options, port_id):
|
||||
exposed_ports = options.get(const.DOCKER_EXPOSED_PORTS_OPTION)
|
||||
if not exposed_ports:
|
||||
return
|
||||
|
||||
sec_group = {
|
||||
'name': utils.get_sg_expose_name(port_id),
|
||||
'description': 'Docker exposed ports created by Kuryr.'
|
||||
}
|
||||
try:
|
||||
sg = app.neutron.create_security_group({'security_group': sec_group})
|
||||
sg_id = sg['security_group']['id']
|
||||
|
||||
except n_exceptions.NeutronClientException as ex:
|
||||
app.logger.error(_LE("Error happend during creating a "
|
||||
"Neutron security group: %s"), ex)
|
||||
raise exceptions.ExportPortFailure(
|
||||
("Could not create required security group {0} "
|
||||
"for setting up exported port ").format(sec_group))
|
||||
|
||||
for exposed in exposed_ports:
|
||||
port = exposed['Port']
|
||||
proto = exposed['Proto']
|
||||
try:
|
||||
proto = const.PROTOCOLS[proto]
|
||||
except KeyError:
|
||||
# This should not happen as Docker client catches such errors
|
||||
app.logger.error(_LE("Unrecognizable protocol %s"), proto)
|
||||
app.neutron.delete_security_group(sg_id)
|
||||
raise exceptions.ExportPortFailure(
|
||||
("Bad protocol number for exposed port. Deleting "
|
||||
"the security group {0}.").format(sg_id))
|
||||
|
||||
sec_group_rule = {
|
||||
'security_group_id': sg_id,
|
||||
'direction': 'ingress',
|
||||
'port_range_min': port,
|
||||
'port_range_max': port,
|
||||
'protocol': proto
|
||||
}
|
||||
|
||||
try:
|
||||
app.neutron.create_security_group_rule({'security_group_rule':
|
||||
sec_group_rule})
|
||||
except n_exceptions.NeutronClientException as ex:
|
||||
app.logger.error(_LE("Error happend during creating a "
|
||||
"Neutron security group "
|
||||
"rule: %s"), ex)
|
||||
app.neutron.delete_security_group(sg_id)
|
||||
raise exceptions.ExportPortFailure(
|
||||
("Could not create required security group rules {0} "
|
||||
"for setting up exported port ").format(sec_group_rule))
|
||||
|
||||
try:
|
||||
sgs = [sg_id]
|
||||
port = app.neutron.show_port(port_id)
|
||||
port = port.get('port')
|
||||
if port:
|
||||
existing_sgs = port.get('security_groups')
|
||||
if existing_sgs:
|
||||
sgs = sgs + existing_sgs
|
||||
|
||||
app.neutron.update_port(port_id,
|
||||
{'port': {'security_groups': sgs}})
|
||||
except n_exceptions.NeutronClientException as ex:
|
||||
app.logger.error(_LE("Error happend during updating a "
|
||||
"Neutron port: %s"), ex)
|
||||
app.neutron.delete_security_group(sg_id)
|
||||
raise exceptions.ExportPortFailure(
|
||||
("Could not update port with required security groups{0} "
|
||||
"for setting up exported port ").format(sgs))
|
||||
|
||||
|
||||
def revoke_expose_ports(port_id):
|
||||
sgs = app.neutron.list_security_groups(
|
||||
name=utils.get_sg_expose_name(port_id))
|
||||
sgs = sgs.get('security_groups')
|
||||
removing_sgs = [sg['id'] for sg in sgs]
|
||||
|
||||
existing_sgs = []
|
||||
port = app.neutron.show_port(port_id)
|
||||
port = port.get('port')
|
||||
if port:
|
||||
existing_sgs = port.get('security_groups')
|
||||
for sg in removing_sgs:
|
||||
if sg in existing_sgs:
|
||||
existing_sgs.remove(sg)
|
||||
try:
|
||||
app.neutron.update_port(port_id,
|
||||
{'port':
|
||||
{'security_groups': existing_sgs}})
|
||||
except n_exceptions.NeutronClientException as ex:
|
||||
app.logger.error(_LE("Error happend during updating a "
|
||||
"Neutron port with a new list of "
|
||||
"security groups: {0}").format(ex))
|
||||
try:
|
||||
for sg in removing_sgs:
|
||||
app.neutron.delete_security_group(sg)
|
||||
except n_exceptions.NeutronClientException as ex:
|
||||
app.logger.error(_LE("Error happend during updating a "
|
||||
"Neutron security group: {0}").format(ex))
|
||||
|
||||
|
||||
@app.route('/Plugin.Activate', methods=['POST'])
|
||||
def plugin_activate():
|
||||
"""Returns the list of the implemented drivers.
|
||||
@ -970,7 +1081,7 @@ def network_driver_leave():
|
||||
|
||||
@app.route('/NetworkDriver.ProgramExternalConnectivity', methods=['POST'])
|
||||
def network_driver_program_external_connectivity():
|
||||
"""Peovides external connectivity fora given container.
|
||||
"""Provides external connectivity for a given container.
|
||||
|
||||
Performs the necessary programming to allow the external connectivity
|
||||
dictated by the specified options
|
||||
@ -981,8 +1092,12 @@ def network_driver_program_external_connectivity():
|
||||
json_data = flask.request.get_json(force=True)
|
||||
app.logger.debug("Received JSON data %s for"
|
||||
" /NetworkDriver.ProgramExternalConnectivity", json_data)
|
||||
# TODO(namix): Add support for exposed ports
|
||||
# TODO(namix): Add support for published ports
|
||||
# TODO(banix): Add support for exposed ports
|
||||
port = _get_neutron_port_from_docker_endpoint(json_data['EndpointID'])
|
||||
if port:
|
||||
_program_expose_ports(json_data['Options'], port)
|
||||
|
||||
# TODO(banix): Add support for published ports
|
||||
return flask.jsonify(const.SCHEMA['SUCCESS'])
|
||||
|
||||
|
||||
@ -999,8 +1114,12 @@ def network_driver_revoke_external_connectivity():
|
||||
json_data = flask.request.get_json(force=True)
|
||||
app.logger.debug("Received JSON data %s for"
|
||||
" /NetworkDriver.RevokeExternalConnectivity", json_data)
|
||||
# TODO(namix): Add support for removal of exposed ports
|
||||
# TODO(namix): Add support for removal of published ports
|
||||
# TODO(banix): Add support for removal of exposed ports
|
||||
port = _get_neutron_port_from_docker_endpoint(json_data['EndpointID'])
|
||||
if port:
|
||||
revoke_expose_ports(port)
|
||||
|
||||
# TODO(banix): Add support for removal of published ports
|
||||
return flask.jsonify(const.SCHEMA['SUCCESS'])
|
||||
|
||||
|
||||
|
182
kuryr_libnetwork/tests/unit/test_external_connectivity.py
Normal file
182
kuryr_libnetwork/tests/unit/test_external_connectivity.py
Normal file
@ -0,0 +1,182 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from kuryr_libnetwork import app
|
||||
from kuryr_libnetwork.common import constants
|
||||
from kuryr_libnetwork.tests.unit import base
|
||||
from kuryr_libnetwork import utils
|
||||
|
||||
PORT = 77
|
||||
PROTOCOL = 6
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestExternalConnectivityKuryr(base.TestKuryrBase):
|
||||
"""The unitests for external connectivity
|
||||
|
||||
This test class is used to test programming and revoking external
|
||||
connectivity for containers. Tests cover containers which already have
|
||||
a security group (perhaps the default security group set up by Neutron)
|
||||
associated with their ports in addition to those without any such
|
||||
security groups. Tests also cover adding more than one port to the
|
||||
list of exposed ports.
|
||||
"""
|
||||
@ddt.data((False, 1), (True, 1), (False, 2), (True, 2))
|
||||
@ddt.unpack
|
||||
def test_network_driver_program_external_connectivity(self, existing_sg,
|
||||
num_ports):
|
||||
fake_docker_net_id = utils.get_hash()
|
||||
fake_docker_endpoint_id = utils.get_hash()
|
||||
|
||||
fake_neutron_net_id = str(uuid.uuid4())
|
||||
fake_neutron_port_id = str(uuid.uuid4())
|
||||
self.mox.StubOutWithMock(app.neutron, 'list_ports')
|
||||
neutron_port_name = utils.get_neutron_port_name(
|
||||
fake_docker_endpoint_id)
|
||||
fake_neutron_v4_subnet_id = str(uuid.uuid4())
|
||||
fake_neutron_v6_subnet_id = str(uuid.uuid4())
|
||||
fake_neutron_ports_response = self._get_fake_ports(
|
||||
fake_docker_endpoint_id, fake_neutron_net_id,
|
||||
fake_neutron_port_id, constants.PORT_STATUS_ACTIVE,
|
||||
fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
|
||||
if existing_sg:
|
||||
fake_neutron_existing_sec_group_id = str(uuid.uuid4())
|
||||
fake_neutron_ports_response['ports'][0]['security_groups'] = [
|
||||
fake_neutron_existing_sec_group_id]
|
||||
|
||||
app.neutron.list_ports(name=neutron_port_name).AndReturn(
|
||||
fake_neutron_ports_response)
|
||||
|
||||
sec_group = {
|
||||
'name': utils.get_sg_expose_name(fake_neutron_port_id),
|
||||
'description': 'Docker exposed ports created by Kuryr.'
|
||||
}
|
||||
self.mox.StubOutWithMock(app.neutron, 'create_security_group')
|
||||
fake_neutron_sec_group_id = utils.get_hash()
|
||||
fake_neutron_sec_group_response = {'security_group':
|
||||
{'id': fake_neutron_sec_group_id}}
|
||||
app.neutron.create_security_group({'security_group':
|
||||
sec_group}).AndReturn(
|
||||
fake_neutron_sec_group_response)
|
||||
|
||||
self.mox.StubOutWithMock(app.neutron, 'create_security_group_rule')
|
||||
for i in range(num_ports):
|
||||
sec_group_rule = {
|
||||
'security_group_id': fake_neutron_sec_group_id,
|
||||
'direction': 'ingress',
|
||||
'port_range_min': PORT + i,
|
||||
'port_range_max': PORT + i,
|
||||
'protocol': constants.PROTOCOLS[PROTOCOL]
|
||||
}
|
||||
app.neutron.create_security_group_rule({'security_group_rule':
|
||||
sec_group_rule})
|
||||
|
||||
sgs = [fake_neutron_sec_group_id]
|
||||
if existing_sg:
|
||||
sgs.append(fake_neutron_existing_sec_group_id)
|
||||
self.mox.StubOutWithMock(app.neutron, 'show_port')
|
||||
app.neutron.show_port(fake_neutron_port_id).AndReturn(
|
||||
{'port': fake_neutron_ports_response['ports'][0]})
|
||||
|
||||
self.mox.StubOutWithMock(app.neutron, 'update_port')
|
||||
app.neutron.update_port(fake_neutron_port_id,
|
||||
{'port': {'security_groups': sgs}})
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
port_opt = []
|
||||
for i in range(num_ports):
|
||||
port_opt.append({u'Port': PORT + i, u'Proto': PROTOCOL})
|
||||
options = {'com.docker.network.endpoint.exposedports':
|
||||
port_opt,
|
||||
'com.docker.network.portmap':
|
||||
[]}
|
||||
data = {
|
||||
'NetworkID': fake_docker_net_id,
|
||||
'EndpointID': fake_docker_endpoint_id,
|
||||
'Options': options,
|
||||
}
|
||||
response = self.app.post('/NetworkDriver.ProgramExternalConnectivity',
|
||||
content_type='application/json',
|
||||
data=jsonutils.dumps(data))
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
decoded_json = jsonutils.loads(response.data)
|
||||
self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json)
|
||||
|
||||
@ddt.data((False), (True))
|
||||
def test_network_driver_revoke_external_connectivity(self, existing_sg):
|
||||
fake_docker_net_id = utils.get_hash()
|
||||
fake_docker_endpoint_id = utils.get_hash()
|
||||
|
||||
fake_neutron_net_id = str(uuid.uuid4())
|
||||
fake_neutron_port_id = str(uuid.uuid4())
|
||||
fake_neutron_sec_group_id = utils.get_hash()
|
||||
self.mox.StubOutWithMock(app.neutron, 'list_ports')
|
||||
neutron_port_name = utils.get_neutron_port_name(
|
||||
fake_docker_endpoint_id)
|
||||
fake_neutron_v4_subnet_id = str(uuid.uuid4())
|
||||
fake_neutron_v6_subnet_id = str(uuid.uuid4())
|
||||
fake_neutron_ports_response = self._get_fake_ports(
|
||||
fake_docker_endpoint_id, fake_neutron_net_id,
|
||||
fake_neutron_port_id, constants.PORT_STATUS_ACTIVE,
|
||||
fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
|
||||
if existing_sg:
|
||||
fake_neutron_existing_sec_group_id = str(uuid.uuid4())
|
||||
fake_neutron_ports_response['ports'][0]['security_groups'] = [
|
||||
fake_neutron_sec_group_id, fake_neutron_existing_sec_group_id]
|
||||
else:
|
||||
fake_neutron_ports_response['ports'][0]['security_groups'] = [
|
||||
fake_neutron_sec_group_id]
|
||||
|
||||
app.neutron.list_ports(name=neutron_port_name).AndReturn(
|
||||
fake_neutron_ports_response)
|
||||
|
||||
self.mox.StubOutWithMock(app.neutron, 'list_security_groups')
|
||||
fake_neutron_sec_group_response = {'security_groups':
|
||||
[{'id': fake_neutron_sec_group_id}]}
|
||||
app.neutron.list_security_groups(
|
||||
name=utils.get_sg_expose_name(fake_neutron_port_id)).AndReturn(
|
||||
fake_neutron_sec_group_response)
|
||||
|
||||
if existing_sg:
|
||||
sgs = [fake_neutron_existing_sec_group_id]
|
||||
else:
|
||||
sgs = []
|
||||
self.mox.StubOutWithMock(app.neutron, 'show_port')
|
||||
app.neutron.show_port(fake_neutron_port_id).AndReturn(
|
||||
{'port': fake_neutron_ports_response['ports'][0]})
|
||||
|
||||
self.mox.StubOutWithMock(app.neutron, 'update_port')
|
||||
app.neutron.update_port(fake_neutron_port_id,
|
||||
{'port': {'security_groups': sgs}})
|
||||
|
||||
self.mox.StubOutWithMock(app.neutron, 'delete_security_group')
|
||||
app.neutron.delete_security_group(fake_neutron_sec_group_id)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = {
|
||||
'NetworkID': fake_docker_net_id,
|
||||
'EndpointID': fake_docker_endpoint_id,
|
||||
}
|
||||
response = self.app.post('/NetworkDriver.RevokeExternalConnectivity',
|
||||
content_type='application/json',
|
||||
data=jsonutils.dumps(data))
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
decoded_json = jsonutils.loads(response.data)
|
||||
self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json)
|
@ -9,7 +9,7 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
@ -31,8 +31,10 @@ from kuryr.lib._i18n import _LE
|
||||
from kuryr.lib import exceptions
|
||||
from kuryr_libnetwork.common import constants as const
|
||||
|
||||
|
||||
DOCKER_NETNS_BASE = '/var/run/docker/netns'
|
||||
PORT_POSTFIX = 'port'
|
||||
SG_POSTFIX = 'exposed_ports'
|
||||
|
||||
|
||||
def get_neutron_client_simple(url, auth_url, token):
|
||||
@ -133,6 +135,15 @@ def get_neutron_subnetpool_name(subnet_cidr):
|
||||
return '-'.join([name_prefix, subnet_cidr])
|
||||
|
||||
|
||||
def get_sg_expose_name(port_id):
|
||||
"""Returns a Neutron security group name.
|
||||
|
||||
:param port_id: The Neutron port id to create a security group for
|
||||
:returns: the Neutron security group name formatted appropriately
|
||||
"""
|
||||
return '-'.join([port_id, SG_POSTFIX])
|
||||
|
||||
|
||||
def get_dict_format_fixed_ips_from_kv_format(fixed_ips):
|
||||
"""Returns fixed_ips in dict format.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user