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:
Jenkins 2016-07-13 09:36:49 +00:00 committed by Gerrit Code Review
commit 1ffffcc401
4 changed files with 332 additions and 13 deletions

View File

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

View File

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

View 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)

View File

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