From 0253740a23d5cb58db539230dd8513b2f845afe1 Mon Sep 17 00:00:00 2001 From: Rossella Sblendido Date: Sat, 24 Aug 2013 00:17:38 +0000 Subject: [PATCH] Implement MidoNet Neutron plugin for Havana Implement L2, L3, security groups, metadata server support for MidoNet Neutron plugin for Havana. blueprint midonet-plugin-havana Change-Id: I0dd1a2ca17d760443c4c7a464a66b6d0a2cf194a --- etc/neutron/plugins/midonet/midonet.ini | 4 +- neutron/plugins/midonet/agent/__init__.py | 16 + .../plugins/midonet/agent/midonet_driver.py | 140 ++ neutron/plugins/midonet/common/__init__.py | 16 + .../plugins/midonet/{ => common}/config.py | 8 +- neutron/plugins/midonet/common/net_util.py | 64 + neutron/plugins/midonet/midonet_lib.py | 1070 +++++---------- neutron/plugins/midonet/plugin.py | 1201 ++++++++++++----- neutron/tests/unit/midonet/mock_lib.py | 58 +- .../tests/unit/midonet/test_midonet_driver.py | 116 ++ .../tests/unit/midonet/test_midonet_lib.py | 259 +--- 11 files changed, 1663 insertions(+), 1289 deletions(-) create mode 100644 neutron/plugins/midonet/agent/__init__.py create mode 100644 neutron/plugins/midonet/agent/midonet_driver.py create mode 100644 neutron/plugins/midonet/common/__init__.py rename neutron/plugins/midonet/{ => common}/config.py (91%) create mode 100644 neutron/plugins/midonet/common/net_util.py create mode 100644 neutron/tests/unit/midonet/test_midonet_driver.py diff --git a/etc/neutron/plugins/midonet/midonet.ini b/etc/neutron/plugins/midonet/midonet.ini index b4573a907..f2e940529 100644 --- a/etc/neutron/plugins/midonet/midonet.ini +++ b/etc/neutron/plugins/midonet/midonet.ini @@ -15,5 +15,5 @@ # Virtual provider router ID # provider_router_id = 00112233-0011-0011-0011-001122334455 -# Virtual metadata router ID -# metadata_router_id = ffeeddcc-ffee-ffee-ffee-ffeeddccbbaa +# Path to midonet host uuid file +# midonet_host_uuid_path = /etc/midolman/host_uuid.properties diff --git a/neutron/plugins/midonet/agent/__init__.py b/neutron/plugins/midonet/agent/__init__.py new file mode 100644 index 000000000..9fddc1976 --- /dev/null +++ b/neutron/plugins/midonet/agent/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2013 Midokura PTE LTD +# All Rights Reserved. +# +# 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. diff --git a/neutron/plugins/midonet/agent/midonet_driver.py b/neutron/plugins/midonet/agent/midonet_driver.py new file mode 100644 index 000000000..728a019fe --- /dev/null +++ b/neutron/plugins/midonet/agent/midonet_driver.py @@ -0,0 +1,140 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2013 Midokura PTE LTD +# All Rights Reserved. +# +# 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. +# +# @author: Rossella Sblendido, Midokura Japan KK +# @author: Tomoe Sugihara, Midokura Japan KK +# @author: Ryu Ishimoto, Midokura Japan KK + +from midonetclient import api +from oslo.config import cfg +from webob import exc as w_exc + +from neutron.agent.linux import dhcp +from neutron.agent.linux import interface +from neutron.agent.linux import ip_lib +from neutron.openstack.common import log as logging +from neutron.plugins.midonet.common import config # noqa + +LOG = logging.getLogger(__name__) + + +class DhcpNoOpDriver(dhcp.DhcpLocalProcess): + + @classmethod + def existing_dhcp_networks(cls, conf, root_helper): + """Return a list of existing networks ids that we have configs for.""" + return [] + + @classmethod + def check_version(cls): + """Execute version checks on DHCP server.""" + return float(1.0) + + def disable(self, retain_port=False): + """Disable DHCP for this network.""" + if not retain_port: + self.device_delegate.destroy(self.network, self.interface_name) + self._remove_config_files() + + def release_lease(self, mac_address, removed_ips): + pass + + def reload_allocations(self): + """Force the DHCP server to reload the assignment database.""" + pass + + def spawn_process(self): + pass + + +class MidonetInterfaceDriver(interface.LinuxInterfaceDriver): + def __init__(self, conf): + super(MidonetInterfaceDriver, self).__init__(conf) + # Read config values + midonet_conf = conf.MIDONET + midonet_uri = midonet_conf.midonet_uri + admin_user = midonet_conf.username + admin_pass = midonet_conf.password + admin_project_id = midonet_conf.project_id + + self.mido_api = api.MidonetApi(midonet_uri, admin_user, + admin_pass, + project_id=admin_project_id) + + def _get_host_uuid(self): + """Get MidoNet host id from host_uuid.properties file.""" + f = open(cfg.CONF.MIDONET.midonet_host_uuid_path) + lines = f.readlines() + host_uuid = filter(lambda x: x.startswith('host_uuid='), + lines)[0].strip()[len('host_uuid='):] + return host_uuid + + def plug(self, network_id, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None): + """This method is called by the Dhcp agent or by the L3 agent + when a new network is created + """ + if not ip_lib.device_exists(device_name, + self.root_helper, + namespace=namespace): + ip = ip_lib.IPWrapper(self.root_helper) + tap_name = device_name.replace(prefix or 'tap', 'tap') + + # Create ns_dev in a namespace if one is configured. + root_dev, ns_dev = ip.add_veth(tap_name, + device_name, + namespace2=namespace) + + ns_dev.link.set_address(mac_address) + + # Add an interface created by ovs to the namespace. + namespace_obj = ip.ensure_namespace(namespace) + namespace_obj.add_device_to_namespace(ns_dev) + + ns_dev.link.set_up() + root_dev.link.set_up() + + vport_id = port_id + host_dev_name = device_name + + # create if-vport mapping. + host_uuid = self._get_host_uuid() + try: + host = self.mido_api.get_host(host_uuid) + except w_exc.HTTPError as e: + LOG.error(_('Failed to create a if-vport mapping on host=%s'), + host_uuid) + raise e + try: + self.mido_api.host.add_host_interface_port( + host, vport_id, host_dev_name) + except w_exc.HTTPError as e: + LOG.warn(_('Faild binding vport=%(vport) to device=%(device)'), + {"vport": vport_id, "device": host_dev_name}) + else: + LOG.warn(_("Device %s already exists"), device_name) + + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + # the port will be deleted by the dhcp agent that will call the plugin + device = ip_lib.IPDevice(device_name, + self.root_helper, + namespace) + device.link.delete() + LOG.debug(_("Unplugged interface '%s'"), device_name) + + ip_lib.IPWrapper( + self.root_helper, namespace).garbage_collect_namespace() diff --git a/neutron/plugins/midonet/common/__init__.py b/neutron/plugins/midonet/common/__init__.py new file mode 100644 index 000000000..9fddc1976 --- /dev/null +++ b/neutron/plugins/midonet/common/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2013 Midokura PTE LTD +# All Rights Reserved. +# +# 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. diff --git a/neutron/plugins/midonet/config.py b/neutron/plugins/midonet/common/config.py similarity index 91% rename from neutron/plugins/midonet/config.py rename to neutron/plugins/midonet/common/config.py index dbeecd95d..79e2a3311 100644 --- a/neutron/plugins/midonet/config.py +++ b/neutron/plugins/midonet/common/config.py @@ -35,12 +35,12 @@ midonet_opts = [ cfg.StrOpt('provider_router_id', default=None, help=_('Virtual provider router ID.')), - cfg.StrOpt('metadata_router_id', - default=None, - help=_('Virtual metadata router ID.')), cfg.StrOpt('mode', default='dev', - help=_('Operational mode. Internal dev use only.')) + help=_('Operational mode. Internal dev use only.')), + cfg.StrOpt('midonet_host_uuid_path', + default='/etc/midolman/host_uuid.properties', + help=_('Path to midonet host uuid file')) ] diff --git a/neutron/plugins/midonet/common/net_util.py b/neutron/plugins/midonet/common/net_util.py new file mode 100644 index 000000000..868eed3a6 --- /dev/null +++ b/neutron/plugins/midonet/common/net_util.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2013 Midokura PTE LTD +# All Rights Reserved. +# +# 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. +# +# @author: Ryu Ishimoto, Midokura Japan KK + + +from neutron.common import constants + + +def subnet_str(cidr): + """Convert the cidr string to x.x.x.x_y format + + :param cidr: CIDR in x.x.x.x/y format + """ + if cidr is None: + return None + return cidr.replace("/", "_") + + +def net_addr(addr): + """Get network address prefix and length from a given address.""" + if addr is None: + return (None, None) + nw_addr, nw_len = addr.split('/') + nw_len = int(nw_len) + return nw_addr, nw_len + + +def get_ethertype_value(ethertype): + """Convert string representation of ethertype to the numerical.""" + if ethertype is None: + return None + mapping = { + 'ipv4': 0x0800, + 'ipv6': 0x86DD, + 'arp': 0x806 + } + return mapping.get(ethertype.lower()) + + +def get_protocol_value(protocol): + """Convert string representation of protocol to the numerical.""" + if protocol is None: + return None + mapping = { + 'tcp': constants.TCP_PROTOCOL, + 'udp': constants.UDP_PROTOCOL, + 'icmp': constants.ICMP_PROTOCOL + } + return mapping.get(protocol.lower()) diff --git a/neutron/plugins/midonet/midonet_lib.py b/neutron/plugins/midonet/midonet_lib.py index d24a38ed2..e5f74dabb 100644 --- a/neutron/plugins/midonet/midonet_lib.py +++ b/neutron/plugins/midonet/midonet_lib.py @@ -18,68 +18,35 @@ # # @author: Tomoe Sugihara, Midokura Japan KK # @author: Ryu Ishimoto, Midokura Japan KK +# @author: Rossella Sblendido, Midokura Japan KK +from midonetclient import midoapi_exceptions from webob import exc as w_exc -from neutron.common import exceptions as q_exc +from neutron.common import exceptions as n_exc from neutron.openstack.common import log as logging - +from neutron.plugins.midonet.common import net_util LOG = logging.getLogger(__name__) -PREFIX = 'OS_SG_' -NAME_IDENTIFIABLE_PREFIX_LEN = len(PREFIX) + 36 # 36 = length of uuid -OS_FLOATING_IP_RULE_KEY = 'OS_FLOATING_IP' -OS_ROUTER_IN_CHAIN_NAME_FORMAT = 'OS_ROUTER_IN_%s' -OS_ROUTER_OUT_CHAIN_NAME_FORMAT = 'OS_ROUTER_OUT_%s' -OS_SG_KEY = 'os_sg_rule_id' -OS_TENANT_ROUTER_RULE_KEY = 'OS_TENANT_ROUTER_RULE' -SNAT_RULE = 'SNAT' -SNAT_RULE_PROPERTY = {OS_TENANT_ROUTER_RULE_KEY: SNAT_RULE} -SUFFIX_IN = '_IN' -SUFFIX_OUT = '_OUT' - - -def sg_label(sg_id, sg_name): - """Construct the security group ID used as chain identifier in MidoNet.""" - return PREFIX + str(sg_id) + '_' + sg_name - - -def sg_rule_properties(os_sg_rule_id): - return {OS_SG_KEY: str(os_sg_rule_id)} - -port_group_name = sg_label - - -def chain_names(sg_id, sg_name): - """Get inbound and outbound chain names.""" - prefix = sg_label(sg_id, sg_name) - in_chain_name = prefix + SUFFIX_IN - out_chain_name = prefix + SUFFIX_OUT - return {'in': in_chain_name, 'out': out_chain_name} - - -def router_chain_names(router_id): - in_name = OS_ROUTER_IN_CHAIN_NAME_FORMAT % router_id - out_name = OS_ROUTER_OUT_CHAIN_NAME_FORMAT % router_id - return {'in': in_name, 'out': out_name} - def handle_api_error(fn): + """Wrapper for methods that throws custom exceptions.""" def wrapped(*args, **kwargs): try: return fn(*args, **kwargs) - except w_exc.HTTPException as ex: + except (w_exc.HTTPException, + midoapi_exceptions.MidoApiConnectionError) as ex: raise MidonetApiException(msg=ex) return wrapped -class MidonetResourceNotFound(q_exc.NotFound): +class MidonetResourceNotFound(n_exc.NotFound): message = _('MidoNet %(resource_type)s %(id)s could not be found') -class MidonetApiException(q_exc.NeutronException): +class MidonetApiException(n_exc.NeutronException): message = _("MidoNet API error: %(msg)s") @@ -140,58 +107,80 @@ class MidoClient: raise MidonetResourceNotFound(resource_type='Bridge', id=id) @handle_api_error - def create_dhcp(self, bridge, gateway_ip, net_addr, net_len): + def create_dhcp(self, bridge, gateway_ip, cidr): """Create a new DHCP entry :param bridge: bridge object to add dhcp to :param gateway_ip: IP address of gateway - :param net_addr: network IP address - :param net_len: network IP address length + :param cidr: subnet represented as x.x.x.x/y :returns: newly created dhcp """ LOG.debug(_("MidoClient.create_dhcp called: bridge=%(bridge)s, " - "net_addr=%(net_addr)s, net_len=%(net_len)s, " - "gateway_ip=%(gateway_ip)s"), - {'bridge': bridge, 'net_addr': net_addr, 'net_len': net_len, - 'gateway_ip': gateway_ip}) + "cidr=%(cidr)s, gateway_ip=%(gateway_ip)s"), + {'bridge': bridge, 'cidr': cidr, 'gateway_ip': gateway_ip}) + net_addr, net_len = net_util.net_addr(cidr) return bridge.add_dhcp_subnet().default_gateway( gateway_ip).subnet_prefix(net_addr).subnet_length( net_len).create() @handle_api_error - def create_dhcp_hosts(self, bridge, ip, mac): - """Create DHCP host entries + def add_dhcp_host(self, bridge, cidr, ip, mac): + """Add DHCP host entry - :param bridge: bridge of the DHCP + :param bridge: bridge the DHCP is configured for + :param cidr: subnet represented as x.x.x.x/y :param ip: IP address :param mac: MAC address """ - LOG.debug(_("MidoClient.create_dhcp_hosts called: bridge=%(bridge)s, " - "ip=%(ip)s, mac=%(mac)s"), {'bridge': bridge, 'ip': ip, - 'mac': mac}) - dhcp_subnets = bridge.get_dhcp_subnets() - if dhcp_subnets: - # Add the host to the first subnet as we currently support one - # subnet per network. - dhcp_subnets[0].add_dhcp_host().ip_addr(ip).mac_addr(mac).create() + LOG.debug(_("MidoClient.add_dhcp_host called: bridge=%(bridge)s, " + "cidr=%(cidr)s, ip=%(ip)s, mac=%(mac)s"), + {'bridge': bridge, 'cidr': cidr, 'ip': ip, 'mac': mac}) + subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr)) + if subnet is None: + raise MidonetApiException(msg=_("Tried to add to" + "non-existent DHCP")) + + subnet.add_dhcp_host().ip_addr(ip).mac_addr(mac).create() @handle_api_error - def delete_dhcp_hosts(self, bridge_id, ip, mac): - """Delete DHCP host entries + def remove_dhcp_host(self, bridge, cidr, ip, mac): + """Remove DHCP host entry - :param bridge_id: id of the bridge of the DHCP + :param bridge: bridge the DHCP is configured for + :param cidr: subnet represented as x.x.x.x/y :param ip: IP address :param mac: MAC address """ - LOG.debug(_("MidoClient.delete_dhcp_hosts called: " - "bridge_id=%(bridge_id)s, ip=%(ip)s, mac=%(mac)s"), - {'bridge_id': bridge_id, 'ip': ip, 'mac': mac}) + LOG.debug(_("MidoClient.remove_dhcp_host called: bridge=%(bridge)s, " + "cidr=%(cidr)s, ip=%(ip)s, mac=%(mac)s"), + {'bridge': bridge, 'cidr': cidr, 'ip': ip, 'mac': mac}) + subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr)) + if subnet is None: + LOG.warn(_("Tried to delete mapping from non-existent subnet")) + return + + for dh in subnet.get_dhcp_hosts(): + if dh.get_mac_addr() == mac and dh.get_ip_addr() == ip: + LOG.debug(_("MidoClient.remove_dhcp_host: Deleting %(dh)r"), + {"dh": dh}) + dh.delete() + + @handle_api_error + def delete_dhcp_host(self, bridge_id, cidr, ip, mac): + """Delete DHCP host entry + + :param bridge_id: id of the bridge of the DHCP + :param cidr: subnet represented as x.x.x.x/y + :param ip: IP address + :param mac: MAC address + """ + LOG.debug(_("MidoClient.delete_dhcp_host called: " + "bridge_id=%(bridge_id)s, cidr=%(cidr)s, ip=%(ip)s, " + "mac=%(mac)s"), {'bridge_id': bridge_id, + 'cidr': cidr, + 'ip': ip, 'mac': mac}) bridge = self.get_bridge(bridge_id) - dhcp_subnets = bridge.get_dhcp_subnets() - if dhcp_subnets: - for dh in dhcp_subnets[0].get_dhcp_hosts(): - if dh.get_mac_addr() == mac and dh.get_ip_addr() == ip: - dh.delete() + self.remove_dhcp_host(bridge, net_util.subnet_str(cidr), ip, mac) @handle_api_error def delete_dhcp(self, bridge): @@ -207,12 +196,17 @@ class MidoClient: dhcp[0].delete() @handle_api_error - def delete_port(self, id): + def delete_port(self, id, delete_chains=False): """Delete a port :param id: id of the port """ - LOG.debug(_("MidoClient.delete_port called: id=%(id)s"), {'id': id}) + LOG.debug(_("MidoClient.delete_port called: id=%(id)s, " + "delete_chains=%(delete_chains)s"), + {'id': id, 'delete_chains': delete_chains}) + if delete_chains: + self.delete_port_chains(id) + self.mido_api.delete_port(id) @handle_api_error @@ -229,26 +223,24 @@ class MidoClient: raise MidonetResourceNotFound(resource_type='Port', id=id) @handle_api_error - def create_exterior_bridge_port(self, bridge): - """Create a new exterior bridge port + def add_bridge_port(self, bridge): + """Add a port on a bridge - :param bridge: bridge object to add port to + :param bridge: Bridge to add a new port to :returns: newly created port """ - LOG.debug(_("MidoClient.create_exterior_bridge_port called: " + LOG.debug(_("MidoClient.add_bridge_port called: " "bridge=%(bridge)s"), {'bridge': bridge}) - return bridge.add_exterior_port().create() + return self.mido_api.add_bridge_port(bridge) @handle_api_error - def create_interior_bridge_port(self, bridge): - """Create a new interior bridge port - - :param bridge: bridge object to add port to - :returns: newly created port - """ - LOG.debug(_("MidoClient.create_interior_bridge_port called: " - "bridge=%(bridge)s"), {'bridge': bridge}) - return bridge.add_interior_port().create() + def add_router_port(self, router, port_address=None, + network_address=None, network_length=None): + """Add a new port to an existing router.""" + return self.mido_api.add_router_port(router, + port_address=port_address, + network_address=network_address, + network_length=network_length) @handle_api_error def create_router(self, tenant_id, name): @@ -264,40 +256,6 @@ class MidoClient: return self.mido_api.add_router().name(name).tenant_id( tenant_id).create() - @handle_api_error - def create_tenant_router(self, tenant_id, name, metadata_router): - """Create a new tenant router - - :param tenant_id: id of tenant creating the router - :param name: name of the router - :param metadata_router: metadata router - :returns: newly created router - """ - LOG.debug(_("MidoClient.create_tenant_router called: " - "tenant_id=%(tenant_id)s, name=%(name)s," - " metadata_router=%(metadata_router)s"), - {'tenant_id': tenant_id, 'name': name, - 'metadata_router': metadata_router}) - router = self.create_router(tenant_id, name) - self.link_router_to_metadata_router(router, metadata_router) - return router - - @handle_api_error - def delete_tenant_router(self, id, metadata_router): - """Delete a tenant router - - :param id: id of router - :param metadata_router: metadata router - """ - LOG.debug(_("MidoClient.delete_tenant_router called: " - "id=%(id)s, metadata_router=%(metadata_router)s"), - {'id': id, 'metadata_router': metadata_router}) - self.unlink_router_from_metadata_router(id, metadata_router) - self.destroy_router_chains(id) - - # delete the router - self.delete_router(id) - @handle_api_error def delete_router(self, id): """Delete a router @@ -336,640 +294,356 @@ class MidoClient: raise MidonetResourceNotFound(resource_type='Router', id=id) @handle_api_error - def link_bridge_port_to_router(self, port_id, router_id, gateway_ip, - net_addr, net_len, metadata_router): - """Link a tenant bridge port to the router + def delete_route(self, id): + return self.mido_api.delete_route(id) - :param port_id: port ID - :param router_id: router id to link to - :param gateway_ip: IP address of gateway - :param net_addr: network IP address - :param net_len: network IP address length - :param metadata_router: metadata router instance + @handle_api_error + def add_dhcp_route_option(self, bridge, cidr, gw_ip, dst_ip): + """Add Option121 route to subnet + + :param bridge: Bridge to add the option route to + :param cidr: subnet represented as x.x.x.x/y + :param gw_ip: IP address of the next hop + :param dst_ip: IP address of the destination, in x.x.x.x/y format """ - LOG.debug(_("MidoClient.link_bridge_port_to_router called: " - "port_id=%(port_id)s, router_id=%(router_id)s, " - "gateway_ip=%(gateway_ip)s net_addr=%(net_addr)s, " - "net_len=%(net_len)s, " - "metadata_router=%(metadata_router)s"), - {'port_id': port_id, 'router_id': router_id, - 'gateway_ip': gateway_ip, 'net_addr': net_addr, - 'net_len': net_len, 'metadata_router': metadata_router}) - router = self.get_router(router_id) + LOG.debug(_("MidoClient.add_dhcp_route_option called: " + "bridge=%(bridge)s, cidr=%(cidr)s, gw_ip=%(gw_ip)s" + "dst_ip=%(dst_ip)s"), + {"bridge": bridge, "cidr": cidr, "gw_ip": gw_ip, + "dst_ip": dst_ip}) + subnet = bridge.get_dhcp_subnet(net_util.subnet_str(cidr)) + if subnet is None: + raise MidonetApiException( + msg=_("Tried to access non-existent DHCP")) + prefix, length = dst_ip.split("/") + routes = [{'destinationPrefix': prefix, 'destinationLength': length, + 'gatewayAddr': gw_ip}] + subnet.opt121_routes(routes).update() - # create an interior port on the router - in_port = router.add_interior_port() - router_port = in_port.port_address(gateway_ip).network_address( - net_addr).network_length(net_len).create() + @handle_api_error + def link(self, port, peer_id): + """Link a port to a given peerId.""" + self.mido_api.link(port, peer_id) - br_port = self.get_port(port_id) - router_port.link(br_port.get_id()) + @handle_api_error + def delete_port_routes(self, routes, port_id): + """Remove routes whose next hop port is the given port ID.""" + for route in routes: + if route.get_next_hop_port() == port_id: + self.mido_api.delete_route(route.get_id()) - # add a route for the subnet in the provider router - router.add_route().type('Normal').src_network_addr( - '0.0.0.0').src_network_length(0).dst_network_addr( - net_addr).dst_network_length(net_len).weight( - 100).next_hop_port(router_port.get_id()).create() + @handle_api_error + def get_router_routes(self, router_id): + """Get all routes for the given router.""" + return self.mido_api.get_router_routes(router_id) - # add a route for the subnet in metadata router; forward - # packets destined to the subnet to the tenant router - for pp in metadata_router.get_peer_ports(): - if pp.get_device_id() == router.get_id(): - mdr_port_id = pp.get_peer_id() - break + @handle_api_error + def unlink(self, port): + """Unlink a port + + :param port: port object + """ + LOG.debug(_("MidoClient.unlink called: port=%(port)s"), + {'port': port}) + if port.get_peer_id(): + self.mido_api.unlink(port) else: - raise Exception( - _("Couldn't find a md router port for the router=%r"), router) - - metadata_router.add_route().type('Normal').src_network_addr( - '0.0.0.0').src_network_length(0).dst_network_addr( - net_addr).dst_network_length(net_len).weight( - 100).next_hop_port(mdr_port_id).create() + LOG.warn(_("Attempted to unlink a port that was not linked. %s"), + port.get_id()) @handle_api_error - def unlink_bridge_port_from_router(self, port_id, net_addr, net_len, - metadata_router): - """Unlink a tenant bridge port from the router + def remove_rules_by_property(self, tenant_id, chain_name, key, value): + """Remove all the rules that match the provided key and value.""" + LOG.debug(_("MidoClient.remove_rules_by_property called: " + "tenant_id=%(tenant_id)s, chain_name=%(chain_name)s" + "key=%(key)s, value=%(value)s"), + {'tenant_id': tenant_id, 'chain_name': chain_name, + 'key': key, 'value': value}) + chain = self.get_chain_by_name(tenant_id, chain_name) + if chain is None: + raise MidonetResourceNotFound(resource_type='Chain', + id=chain_name) - :param bridge_id: bridge ID - :param net_addr: network IP address - :param net_len: network IP address length - :param metadata_router: metadata router instance + for r in chain.get_rules(): + if key in r.get_properties(): + if r.get_properties()[key] == value: + self.mido_api.delete_rule(r.get_id()) + + @handle_api_error + def add_router_chains(self, router, inbound_chain_name, + outbound_chain_name): + """Create chains for a new router. + + Creates inbound and outbound chains for the router with the given + names, and the new chains are set on the router. + + :param router: router to set chains for + :param inbound_chain_name: Name of the inbound chain + :param outbound_chain_name: Name of the outbound chain """ - LOG.debug(_("MidoClient.unlink_bridge_port_from_router called: " - "port_id=%(port_id)s, net_addr=%(net_addr)s, " - "net_len=%(net_len)s, " - "metadata_router=%(metadata_router)s"), - {'port_id': port_id, 'net_addr': net_addr, - 'net_len': net_len, 'metadata_router': metadata_router}) - port = self.get_port(port_id) - port.unlink() - self.delete_port(port.get_peer_id()) - self.delete_port(port.get_id()) + LOG.debug(_("MidoClient.create_router_chains called: " + "router=%(router)s, inbound_chain_name=%(in_chain)s, " + "outbound_chain_name=%(out_chain)s"), + {"router": router, "in_chain": inbound_chain_name, + "out_chain": outbound_chain_name}) + tenant_id = router.get_tenant_id() - # delete the route for the subnet in the metadata router - for r in metadata_router.get_routes(): - if (r.get_dst_network_addr() == net_addr and - r.get_dst_network_length() == net_len): - LOG.debug(_('Deleting route=%r ...'), r) - self.mido_api.delete_route(r.get_id()) + inbound_chain = self.mido_api.add_chain().tenant_id(tenant_id).name( + inbound_chain_name,).create() + outbound_chain = self.mido_api.add_chain().tenant_id(tenant_id).name( + outbound_chain_name).create() + + # set chains to in/out filters + router.inbound_filter_id(inbound_chain.get_id()).outbound_filter_id( + outbound_chain.get_id()).update() + return inbound_chain, outbound_chain + + @handle_api_error + def delete_router_chains(self, id): + """Deletes chains of a router. + + :param id: router ID to delete chains of + """ + LOG.debug(_("MidoClient.delete_router_chains called: " + "id=%(id)s"), {'id': id}) + router = self.get_router(id) + if (router.get_inbound_filter_id()): + self.mido_api.delete_chain(router.get_inbound_filter_id()) + + if (router.get_outbound_filter_id()): + self.mido_api.delete_chain(router.get_outbound_filter_id()) + + @handle_api_error + def delete_port_chains(self, id): + """Deletes chains of a port. + + :param id: port ID to delete chains of + """ + LOG.debug(_("MidoClient.delete_port_chains called: " + "id=%(id)s"), {'id': id}) + port = self.get_port(id) + if (port.get_inbound_filter_id()): + self.mido_api.delete_chain(port.get_inbound_filter_id()) + + if (port.get_outbound_filter_id()): + self.mido_api.delete_chain(port.get_outbound_filter_id()) + + @handle_api_error + def get_link_port(self, router, peer_router_id): + """Setup a route on the router to the next hop router.""" + LOG.debug(_("MidoClient.get_link_port called: " + "router=%(router)s, peer_router_id=%(peer_router_id)s"), + {'router': router, 'peer_router_id': peer_router_id}) + # Find the port linked between the two routers + link_port = None + for p in router.get_peer_ports(): + if p.get_device_id() == peer_router_id: + link_port = p break + return link_port @handle_api_error - def link_bridge_to_provider_router(self, bridge, provider_router, - gateway_ip, net_addr, net_len): - """Link a tenant bridge to the provider router - - :param bridge: tenant bridge - :param provider_router: provider router to link to - :param gateway_ip: IP address of gateway - :param net_addr: network IP address - :param net_len: network IP address length - """ - LOG.debug(_("MidoClient.link_bridge_to_provider_router called: " - "bridge=%(bridge)s, provider_router=%(provider_router)s, " - "gateway_ip=%(gateway_ip)s, net_addr=%(net_addr)s, " - "net_len=%(net_len)s"), - {'bridge': bridge, 'provider_router': provider_router, - 'gateway_ip': gateway_ip, 'net_addr': net_addr, - 'net_len': net_len}) - # create an interior port on the provider router - in_port = provider_router.add_interior_port() - pr_port = in_port.port_address(gateway_ip).network_address( - net_addr).network_length(net_len).create() - - # create an interior bridge port, then link it to the router. - br_port = bridge.add_interior_port().create() - pr_port.link(br_port.get_id()) - - # add a route for the subnet in the provider router - provider_router.add_route().type('Normal').src_network_addr( - '0.0.0.0').src_network_length(0).dst_network_addr( - net_addr).dst_network_length(net_len).weight( - 100).next_hop_port(pr_port.get_id()).create() + def add_router_route(self, router, type='Normal', + src_network_addr=None, src_network_length=None, + dst_network_addr=None, dst_network_length=None, + next_hop_port=None, next_hop_gateway=None, + weight=100): + """Setup a route on the router.""" + return self.mido_api.add_router_route( + router, type=type, src_network_addr=src_network_addr, + src_network_length=src_network_length, + dst_network_addr=dst_network_addr, + dst_network_length=dst_network_length, + next_hop_port=next_hop_port, next_hop_gateway=next_hop_gateway, + weight=weight) @handle_api_error - def unlink_bridge_from_provider_router(self, bridge, provider_router): - """Unlink a tenant bridge from the provider router + def add_static_nat(self, tenant_id, chain_name, from_ip, to_ip, port_id, + nat_type='dnat', **kwargs): + """Add a static NAT entry - :param bridge: tenant bridge - :param provider_router: provider router to link to + :param tenant_id: owner fo the chain to add a NAT to + :param chain_name: name of the chain to add a NAT to + :param from_ip: IP to translate from + :param from_ip: IP to translate from + :param to_ip: IP to translate to + :param port_id: port to match on + :param nat_type: 'dnat' or 'snat' """ - LOG.debug(_("MidoClient.unlink_bridge_from_provider_router called: " - "bridge=%(bridge)s, provider_router=%(provider_router)s"), - {'bridge': bridge, 'provider_router': provider_router}) - # Delete routes and unlink the router and the bridge. - routes = provider_router.get_routes() + LOG.debug(_("MidoClient.add_static_nat called: " + "tenant_id=%(tenant_id)s, chain_name=%(chain_name)s, " + "from_ip=%(from_ip)s, to_ip=%(to_ip)s, " + "port_id=%(port_id)s, nat_type=%(nat_type)s"), + {'tenant_id': tenant_id, 'chain_name': chain_name, + 'from_ip': from_ip, 'to_ip': to_ip, + 'portid': port_id, 'nat_type': nat_type}) + if nat_type not in ['dnat', 'snat']: + raise ValueError(_("Invalid NAT type passed in %s") % nat_type) - bridge_ports_to_delete = [ - p for p in provider_router.get_peer_ports() - if p.get_device_id() == bridge.get_id()] + chain = self.get_chain_by_name(tenant_id, chain_name) + nat_targets = [] + nat_targets.append( + {'addressFrom': to_ip, 'addressTo': to_ip, + 'portFrom': 0, 'portTo': 0}) - for p in bridge.get_peer_ports(): - if p.get_device_id() == provider_router.get_id(): - # delete the routes going to the bridge - for r in routes: - if r.get_next_hop_port() == p.get_id(): - self.mido_api.delete_route(r.get_id()) - p.unlink() - self.mido_api.delete_port(p.get_id()) + rule = chain.add_rule().type(nat_type).flow_action('accept').position( + 1).nat_targets(nat_targets).properties(kwargs) - # delete bridge port - for port in bridge_ports_to_delete: - self.mido_api.delete_port(port.get_id()) + if nat_type == 'dnat': + rule = rule.nw_dst_address(from_ip).nw_dst_length(32).in_ports( + [port_id]) + else: + rule = rule.nw_src_address(from_ip).nw_src_length(32).out_ports( + [port_id]) + + return rule.create() @handle_api_error - def set_router_external_gateway(self, id, provider_router, snat_ip): - """Set router external gateway + def add_dynamic_snat(self, tenant_id, pre_chain_name, post_chain_name, + snat_ip, port_id, **kwargs): + """Add SNAT masquerading rule - :param ID: ID of the tenant router - :param provider_router: provider router - :param snat_ip: SNAT IP address + MidoNet requires two rules on the router, one to do NAT to a range of + ports, and another to retrieve back the original IP in the return + flow. """ - LOG.debug(_("MidoClient.set_router_external_gateway called: " - "id=%(id)s, provider_router=%(provider_router)s, " - "snat_ip=%(snat_ip)s)"), - {'id': id, 'provider_router': provider_router, - 'snat_ip': snat_ip}) - tenant_router = self.get_router(id) + pre_chain = self.get_chain_by_name(tenant_id, pre_chain_name) + post_chain = self.get_chain_by_name(tenant_id, post_chain_name) - # Create a interior port in the provider router - in_port = provider_router.add_interior_port() - pr_port = in_port.network_address( - '169.254.255.0').network_length(30).port_address( - '169.254.255.1').create() - - # Create a port in the tenant router - tr_port = tenant_router.add_interior_port().network_address( - '169.254.255.0').network_length(30).port_address( - '169.254.255.2').create() - - # Link them - pr_port.link(tr_port.get_id()) - - # Add a route for snat_ip to bring it down to tenant - provider_router.add_route().type( - 'Normal').src_network_addr('0.0.0.0').src_network_length( - 0).dst_network_addr(snat_ip).dst_network_length( - 32).weight(100).next_hop_port( - pr_port.get_id()).create() - - # Add default route to uplink in the tenant router - tenant_router.add_route().type('Normal').src_network_addr( - '0.0.0.0').src_network_length(0).dst_network_addr( - '0.0.0.0').dst_network_length(0).weight( - 100).next_hop_port(tr_port.get_id()).create() - - # ADD SNAT(masquerade) rules - chains = self.get_router_chains( - tenant_router.get_tenant_id(), tenant_router.get_id()) - - chains['in'].add_rule().nw_dst_address(snat_ip).nw_dst_length( + pre_chain.add_rule().nw_dst_address(snat_ip).nw_dst_length( 32).type('rev_snat').flow_action('accept').in_ports( - [tr_port.get_id()]).properties( - SNAT_RULE_PROPERTY).position(1).create() + [port_id]).properties(kwargs).position(1).create() nat_targets = [] nat_targets.append( {'addressFrom': snat_ip, 'addressTo': snat_ip, 'portFrom': 1, 'portTo': 65535}) - chains['out'].add_rule().type('snat').flow_action( + post_chain.add_rule().type('snat').flow_action( 'accept').nat_targets(nat_targets).out_ports( - [tr_port.get_id()]).properties( - SNAT_RULE_PROPERTY).position(1).create() + [port_id]).properties(kwargs).position(1).create() @handle_api_error - def clear_router_external_gateway(self, id): - """Clear router external gateway + def remove_static_route(self, router, ip): + """Remove static route for the IP - :param ID: ID of the tenant router + :param router: next hop router to remove the routes to + :param ip: IP address of the route to remove """ - LOG.debug(_("MidoClient.clear_router_external_gateway called: " - "id=%(id)s"), {'id': id}) - tenant_router = self.get_router(id) - - # delete the port that is connected to provider router - for p in tenant_router.get_ports(): - if p.get_port_address() == '169.254.255.2': - peer_port_id = p.get_peer_id() - p.unlink() - self.mido_api.delete_port(peer_port_id) - self.mido_api.delete_port(p.get_id()) - - # delete default route - for r in tenant_router.get_routes(): - if (r.get_dst_network_addr() == '0.0.0.0' and - r.get_dst_network_length() == 0): - self.mido_api.delete_route(r.get_id()) - - # delete SNAT(masquerade) rules - chains = self.get_router_chains( - tenant_router.get_tenant_id(), - tenant_router.get_id()) - - for r in chains['in'].get_rules(): - if OS_TENANT_ROUTER_RULE_KEY in r.get_properties(): - if r.get_properties()[ - OS_TENANT_ROUTER_RULE_KEY] == SNAT_RULE: - self.mido_api.delete_rule(r.get_id()) - - for r in chains['out'].get_rules(): - if OS_TENANT_ROUTER_RULE_KEY in r.get_properties(): - if r.get_properties()[ - OS_TENANT_ROUTER_RULE_KEY] == SNAT_RULE: - self.mido_api.delete_rule(r.get_id()) - - @handle_api_error - def get_router_chains(self, tenant_id, router_id): - """Get router chains. - - Returns a dictionary that has in/out chain resources key'ed with 'in' - and 'out' respectively, given the tenant_id and the router_id passed - in in the arguments. - """ - LOG.debug(_("MidoClient.get_router_chains called: " - "tenant_id=%(tenant_id)s router_id=%(router_id)s"), - {'tenant_id': tenant_id, 'router_id': router_id}) - - chain_names = router_chain_names(router_id) - chains = {} - for c in self.mido_api.get_chains({'tenant_id': tenant_id}): - if c.get_name() == chain_names['in']: - chains['in'] = c - elif c.get_name() == chain_names['out']: - chains['out'] = c - return chains - - @handle_api_error - def create_router_chains(self, router): - """Create chains for a new router. - - Creates chains for the router and returns the same dictionary as - get_router_chains() returns. - - :param router: router to set chains for - """ - LOG.debug(_("MidoClient.create_router_chains called: " - "router=%(router)s"), {'router': router}) - chains = {} - router_id = router.get_id() - tenant_id = router.get_tenant_id() - chain_names = router_chain_names(router_id) - chains['in'] = self.mido_api.add_chain().tenant_id(tenant_id).name( - chain_names['in']).create() - - chains['out'] = self.mido_api.add_chain().tenant_id(tenant_id).name( - chain_names['out']).create() - - # set chains to in/out filters - router.inbound_filter_id( - chains['in'].get_id()).outbound_filter_id( - chains['out'].get_id()).update() - return chains - - @handle_api_error - def destroy_router_chains(self, id): - """Deletes chains of a router. - - :param id: router ID to delete chains of - """ - LOG.debug(_("MidoClient.destroy_router_chains called: " - "id=%(id)s"), {'id': id}) - # delete corresponding chains - router = self.get_router(id) - chains = self.get_router_chains(router.get_tenant_id(), id) - self.mido_api.delete_chain(chains['in'].get_id()) - self.mido_api.delete_chain(chains['out'].get_id()) - - @handle_api_error - def link_router_to_metadata_router(self, router, metadata_router): - """Link a router to the metadata router - - :param router: router to link - :param metadata_router: metadata router - """ - LOG.debug(_("MidoClient.link_router_to_metadata_router called: " - "router=%(router)s, metadata_router=%(metadata_router)s"), - {'router': router, 'metadata_router': metadata_router}) - # link to metadata router - in_port = metadata_router.add_interior_port() - mdr_port = in_port.network_address('169.254.255.0').network_length( - 30).port_address('169.254.255.1').create() - - tr_port = router.add_interior_port().network_address( - '169.254.255.0').network_length(30).port_address( - '169.254.255.2').create() - mdr_port.link(tr_port.get_id()) - - # forward metadata traffic to metadata router - router.add_route().type('Normal').src_network_addr( - '0.0.0.0').src_network_length(0).dst_network_addr( - '169.254.169.254').dst_network_length(32).weight( - 100).next_hop_port(tr_port.get_id()).create() - - @handle_api_error - def unlink_router_from_metadata_router(self, id, metadata_router): - """Unlink a router from the metadata router - - :param id: ID of router - :param metadata_router: metadata router - """ - LOG.debug(_("MidoClient.unlink_router_from_metadata_router called: " - "id=%(id)s, metadata_router=%(metadata_router)s"), - {'id': id, 'metadata_router': metadata_router}) - # unlink from metadata router and delete the interior ports - # that connect metadata router and this router. - for pp in metadata_router.get_peer_ports(): - if pp.get_device_id() == id: - mdr_port = self.get_port(pp.get_peer_id()) - pp.unlink() - self.mido_api.delete_port(pp.get_id()) - self.mido_api.delete_port(mdr_port.get_id()) - - @handle_api_error - def setup_floating_ip(self, router_id, provider_router, floating_ip, - fixed_ip, identifier): - """Setup MidoNet for floating IP - - :param router_id: router_id - :param provider_router: provider router - :param floating_ip: floating IP address - :param fixed_ip: fixed IP address - :param identifier: identifier to use to map to MidoNet - """ - LOG.debug(_("MidoClient.setup_floating_ip called: " - "router_id=%(router_id)s, " - "provider_router=%(provider_router)s" - "floating_ip=%(floating_ip)s, fixed_ip=%(fixed_ip)s" - "identifier=%(identifier)s"), - {'router_id': router_id, 'provider_router': provider_router, - 'floating_ip': floating_ip, 'fixed_ip': fixed_ip, - 'identifier': identifier}) - # unlink from metadata router and delete the interior ports - router = self.mido_api.get_router(router_id) - # find the provider router port that is connected to the tenant - # of the floating ip - for p in router.get_peer_ports(): - if p.get_device_id() == provider_router.get_id(): - pr_port = p - - # get the tenant router port id connected to provider router - tr_port_id = pr_port.get_peer_id() - - # add a route for the floating ip to bring it to the tenant - provider_router.add_route().type( - 'Normal').src_network_addr('0.0.0.0').src_network_length( - 0).dst_network_addr( - floating_ip).dst_network_length( - 32).weight(100).next_hop_port( - pr_port.get_id()).create() - - chains = self.get_router_chains(router.get_tenant_id(), router_id) - - # add dnat/snat rule pair for the floating ip - nat_targets = [] - nat_targets.append( - {'addressFrom': fixed_ip, 'addressTo': fixed_ip, - 'portFrom': 0, 'portTo': 0}) - - floating_property = {OS_FLOATING_IP_RULE_KEY: identifier} - chains['in'].add_rule().nw_dst_address( - floating_ip).nw_dst_length(32).type( - 'dnat').flow_action('accept').nat_targets( - nat_targets).in_ports([tr_port_id]).position( - 1).properties(floating_property).create() - - nat_targets = [] - nat_targets.append( - {'addressFrom': floating_ip, 'addressTo': floating_ip, - 'portFrom': 0, 'portTo': 0}) - - chains['out'].add_rule().nw_src_address( - fixed_ip).nw_src_length(32).type( - 'snat').flow_action('accept').nat_targets( - nat_targets).out_ports( - [tr_port_id]).position(1).properties( - floating_property).create() - - @handle_api_error - def clear_floating_ip(self, router_id, provider_router, floating_ip, - identifier): - """Remove floating IP - - :param router_id: router_id - :param provider_router: provider router - :param floating_ip: floating IP address - :param identifier: identifier to use to map to MidoNet - """ - LOG.debug(_("MidoClient.clear_floating_ip called: " - "router_id=%(router_id)s, " - "provider_router=%(provider_router)s" - "floating_ip=%(floating_ip)s, identifier=%(identifier)s"), - {'router_id': router_id, 'provider_router': provider_router, - 'floating_ip': floating_ip, 'identifier': identifier}) - router = self.mido_api.get_router(router_id) - - # find the provider router port that is connected to the tenant - # delete the route for this floating ip - for r in provider_router.get_routes(): - if (r.get_dst_network_addr() == floating_ip and + LOG.debug(_("MidoClient.remote_static_route called: " + "router=%(router)s, ip=%(ip)s"), + {'router': router, 'ip': ip}) + for r in router.get_routes(): + if (r.get_dst_network_addr() == ip and r.get_dst_network_length() == 32): self.mido_api.delete_route(r.get_id()) - # delete snat/dnat rule pair for this floating ip - chains = self.get_router_chains(router.get_tenant_id(), router_id) - - for r in chains['in'].get_rules(): - if OS_FLOATING_IP_RULE_KEY in r.get_properties(): - if r.get_properties()[OS_FLOATING_IP_RULE_KEY] == identifier: - LOG.debug(_('deleting rule=%r'), r) - self.mido_api.delete_rule(r.get_id()) - break - - for r in chains['out'].get_rules(): - if OS_FLOATING_IP_RULE_KEY in r.get_properties(): - if r.get_properties()[OS_FLOATING_IP_RULE_KEY] == identifier: - LOG.debug(_('deleting rule=%r'), r) - self.mido_api.delete_rule(r.get_id()) - break + @handle_api_error + def update_port_chains(self, port, inbound_chain_id, outbound_chain_id): + """Bind inbound and outbound chains to the port.""" + LOG.debug(_("MidoClient.update_port_chains called: port=%(port)s" + "inbound_chain_id=%(inbound_chain_id)s, " + "outbound_chain_id=%(outbound_chain_id)s"), + {"port": port, "inbound_chain_id": inbound_chain_id, + "outbound_chain_id": outbound_chain_id}) + port.inbound_filter_id(inbound_chain_id).outbound_filter_id( + outbound_chain_id).update() @handle_api_error - def create_for_sg(self, tenant_id, sg_id, sg_name): - """Create a new chain for security group. - - Creating a security group creates a pair of chains in MidoNet, one for - inbound and the other for outbound. - """ - LOG.debug(_("MidoClient.create_for_sg called: " - "tenant_id=%(tenant_id)s sg_id=%(sg_id)s " - "sg_name=%(sg_name)s "), - {'tenant_id': tenant_id, 'sg_id': sg_id, 'sg_name': sg_name}) - - cnames = chain_names(sg_id, sg_name) - self.mido_api.add_chain().tenant_id(tenant_id).name( - cnames['in']).create() - self.mido_api.add_chain().tenant_id(tenant_id).name( - cnames['out']).create() - - pg_name = port_group_name(sg_id, sg_name) - self.mido_api.add_port_group().tenant_id(tenant_id).name( - pg_name).create() + def create_chain(self, tenant_id, name): + """Create a new chain.""" + LOG.debug(_("MidoClient.create_chain called: tenant_id=%(tenant_id)s " + " name=%(name)s"), {"tenant_id": tenant_id, "name": name}) + return self.mido_api.add_chain().tenant_id(tenant_id).name( + name).create() @handle_api_error - def delete_for_sg(self, tenant_id, sg_id, sg_name): - """Delete a chain mapped to a security group. + def delete_chain(self, id): + """Delete chain matching the ID.""" + LOG.debug(_("MidoClient.delete_chain called: id=%(id)s"), {"id": id}) + self.mido_api.delete_chain(id) - Delete a SG means deleting all the chains (inbound and outbound) - associated with the SG in MidoNet. - """ - LOG.debug(_("MidoClient.delete_for_sg called: " - "tenant_id=%(tenant_id)s sg_id=%(sg_id)s " - "sg_name=%(sg_name)s "), - {'tenant_id': tenant_id, 'sg_id': sg_id, 'sg_name': sg_name}) - - cnames = chain_names(sg_id, sg_name) + @handle_api_error + def delete_chains_by_names(self, tenant_id, names): + """Delete chains matching the names given for a tenant.""" + LOG.debug(_("MidoClient.delete_chains_by_names called: " + "tenant_id=%(tenant_id)s names=%(names)s "), + {"tenant_id": tenant_id, "names": names}) chains = self.mido_api.get_chains({'tenant_id': tenant_id}) for c in chains: - if c.get_name() == cnames['in'] or c.get_name() == cnames['out']: - LOG.debug(_('MidoClient.delete_for_sg: deleting chain=%r'), - c.get_id()) + if c.get_name() in names: self.mido_api.delete_chain(c.get_id()) - pg_name = port_group_name(sg_id, sg_name) - pgs = self.mido_api.get_port_groups({'tenant_id': tenant_id}) - for pg in pgs: - if pg.get_name() == pg_name: - LOG.debug(_("MidoClient.delete_for_sg: deleting pg=%r"), - pg) - self.mido_api.delete_port_group(pg.get_id()) - @handle_api_error - def get_sg_chains(self, tenant_id, sg_id): - """Get a list of chains mapped to a security group.""" - LOG.debug(_("MidoClient.get_sg_chains called: " - "tenant_id=%(tenant_id)s sg_id=%(sg_id)s"), - {'tenant_id': tenant_id, 'sg_id': sg_id}) - - cnames = chain_names(sg_id, sg_name='') - chain_name_prefix_for_id = cnames['in'][:NAME_IDENTIFIABLE_PREFIX_LEN] - chains = {} - + def get_chain_by_name(self, tenant_id, name): + """Get the chain by its name.""" + LOG.debug(_("MidoClient.get_chain_by_name called: " + "tenant_id=%(tenant_id)s name=%(name)s "), + {"tenant_id": tenant_id, "name": name}) for c in self.mido_api.get_chains({'tenant_id': tenant_id}): - if c.get_name().startswith(chain_name_prefix_for_id): - if c.get_name().endswith(SUFFIX_IN): - chains['in'] = c - if c.get_name().endswith(SUFFIX_OUT): - chains['out'] = c - assert 'in' in chains - assert 'out' in chains - return chains - - @handle_api_error - def get_port_groups_for_sg(self, tenant_id, sg_id): - LOG.debug(_("MidoClient.get_port_groups_for_sg called: " - "tenant_id=%(tenant_id)s sg_id=%(sg_id)s"), - {'tenant_id': tenant_id, 'sg_id': sg_id}) - - pg_name_prefix = port_group_name( - sg_id, sg_name='')[:NAME_IDENTIFIABLE_PREFIX_LEN] - port_groups = self.mido_api.get_port_groups({'tenant_id': tenant_id}) - for pg in port_groups: - if pg.get_name().startswith(pg_name_prefix): - LOG.debug(_( - "MidoClient.get_port_groups_for_sg exiting: pg=%r"), pg) - return pg + if c.get_name() == name: + return c return None @handle_api_error - def create_for_sg_rule(self, rule): - LOG.debug(_("MidoClient.create_for_sg_rule called: rule=%r"), rule) - - direction = rule['direction'] - protocol = rule['protocol'] - port_range_max = rule['port_range_max'] - rule_id = rule['id'] - security_group_id = rule['security_group_id'] - remote_group_id = rule['remote_group_id'] - remote_ip_prefix = rule['remote_ip_prefix'] # watch out. not validated - tenant_id = rule['tenant_id'] - port_range_min = rule['port_range_min'] - - # construct a corresponding rule - tp_src_start = tp_src_end = None - tp_dst_start = tp_dst_end = None - nw_src_address = None - nw_src_length = None - port_group_id = None - - # handle source - if remote_ip_prefix is not None: - nw_src_address, nw_src_length = remote_ip_prefix.split('/') - elif remote_group_id is not None: # security group as a source - source_pg = self.pg_manager.get_for_sg(tenant_id, remote_group_id) - port_group_id = source_pg.get_id() - else: - raise Exception(_("Don't know what to do with rule=%r"), rule) - - # dst ports - tp_dst_start, tp_dst_end = port_range_min, port_range_max - - # protocol - if protocol == 'tcp': - nw_proto = 6 - elif protocol == 'udp': - nw_proto = 17 - elif protocol == 'icmp': - nw_proto = 1 - # extract type and code from reporposed fields - icmp_type = rule['from_port'] - icmp_code = rule['to_port'] - - # translate -1(wildcard in OS) to midonet wildcard - if icmp_type == -1: - icmp_type = None - if icmp_code == -1: - icmp_code = None - - # set data for midonet rule - tp_src_start = tp_src_end = icmp_type - tp_dst_start = tp_dst_end = icmp_code - - chains = self.get_sg_chains(tenant_id, security_group_id) - chain = None - if direction == 'egress': - chain = chains['in'] - elif direction == 'ingress': - chain = chains['out'] - else: - raise Exception(_("Don't know what to do with rule=%r"), rule) - - # create an accept rule - properties = sg_rule_properties(rule_id) - LOG.debug(_("MidoClient.create_for_sg_rule: adding accept rule " - "%(rule_id)s in portgroup %(port_group_id)s"), - {'rule_id': rule_id, 'port_group_id': port_group_id}) - chain.add_rule().port_group(port_group_id).type('accept').nw_proto( - nw_proto).nw_src_address(nw_src_address).nw_src_length( - nw_src_length).tp_src_start(tp_src_start).tp_src_end( - tp_src_end).tp_dst_start(tp_dst_start).tp_dst_end( - tp_dst_end).properties(properties).create() + def get_port_group_by_name(self, tenant_id, name): + """Get the port group by name.""" + LOG.debug(_("MidoClient.get_port_group_by_name called: " + "tenant_id=%(tenant_id)s name=%(name)s "), + {"tenant_id": tenant_id, "name": name}) + for p in self.mido_api.get_port_groups({'tenant_id': tenant_id}): + if p.get_name() == name: + return p + return None @handle_api_error - def delete_for_sg_rule(self, rule): - LOG.debug(_("MidoClient.delete_for_sg_rule called: rule=%r"), rule) + def create_port_group(self, tenant_id, name): + """Create a port group - tenant_id = rule['tenant_id'] - security_group_id = rule['security_group_id'] - rule_id = rule['id'] + Create a new port group for a given name and ID. + """ + LOG.debug(_("MidoClient.create_port_group called: " + "tenant_id=%(tenant_id)s name=%(name)s"), + {"tenant_id": tenant_id, "name": name}) + return self.mido_api.add_port_group().tenant_id(tenant_id).name( + name).create() - properties = sg_rule_properties(rule_id) - # search for the chains to find the rule to delete - chains = self.get_sg_chains(tenant_id, security_group_id) - for c in chains['in'], chains['out']: - rules = c.get_rules() - for r in rules: - if r.get_properties() == properties: - LOG.debug(_("MidoClient.delete_for_sg_rule: deleting " - "rule %r"), r) - self.mido_api.delete_rule(r.get_id()) + @handle_api_error + def delete_port_group_by_name(self, tenant_id, name): + """Delete port group matching the name given for a tenant.""" + LOG.debug(_("MidoClient.delete_port_group_by_name called: " + "tenant_id=%(tenant_id)s name=%(name)s "), + {"tenant_id": tenant_id, "name": name}) + pgs = self.mido_api.get_port_groups({'tenant_id': tenant_id}) + for pg in pgs: + if pg.get_name() == name: + LOG.debug(_("Deleting pg %(id)s"), {"id": pg.get_id()}) + self.mido_api.delete_port_group(pg.get_id()) + + @handle_api_error + def add_port_to_port_group_by_name(self, tenant_id, name, port_id): + """Add a port to a port group with the given name.""" + LOG.debug(_("MidoClient.add_port_to_port_group_by_name called: " + "tenant_id=%(tenant_id)s name=%(name)s " + "port_id=%(port_id)s"), + {"tenant_id": tenant_id, "name": name, "port_id": port_id}) + pg = self.get_port_group_by_name(tenant_id, name) + if pg is None: + raise MidonetResourceNotFound(resource_type='PortGroup', id=name) + + pg = pg.add_port_group_port().port_id(port_id).create() + return pg + + @handle_api_error + def remove_port_from_port_groups(self, port_id): + """Remove a port binding from all the port groups.""" + LOG.debug(_("MidoClient.remove_port_from_port_groups called: " + "port_id=%(port_id)s"), {"port_id": port_id}) + port = self.get_port(port_id) + for pg in port.get_port_groups(): + pg.delete() + + @handle_api_error + def add_chain_rule(self, chain, action='accept', **kwargs): + """Create a new accept chain rule.""" + self.mido_api.add_chain_rule(chain, action, **kwargs) diff --git a/neutron/plugins/midonet/plugin.py b/neutron/plugins/midonet/plugin.py index 05b9ef698..4dd15d8dd 100644 --- a/neutron/plugins/midonet/plugin.py +++ b/neutron/plugins/midonet/plugin.py @@ -19,71 +19,344 @@ # @author: Takaaki Suzuki, Midokura Japan KK # @author: Tomoe Sugihara, Midokura Japan KK # @author: Ryu Ishimoto, Midokura Japan KK +# @author: Rossella Sblendido, Midokura Japan KK from midonetclient import api from oslo.config import cfg -from neutron.common import exceptions as q_exc +from neutron.common import constants +from neutron.common import exceptions as n_exc +from neutron.common import rpc as n_rpc +from neutron.common import topics +from neutron.db import agents_db +from neutron.db import agentschedulers_db from neutron.db import api as db from neutron.db import db_base_plugin_v2 +from neutron.db import dhcp_rpc_base from neutron.db import l3_db from neutron.db import models_v2 from neutron.db import securitygroups_db from neutron.extensions import securitygroup as ext_sg +from neutron.openstack.common import excutils from neutron.openstack.common import log as logging -from neutron.plugins.midonet import config # noqa +from neutron.openstack.common import rpc +from neutron.plugins.midonet.common import config # noqa +from neutron.plugins.midonet.common import net_util from neutron.plugins.midonet import midonet_lib - LOG = logging.getLogger(__name__) +METADATA_DEFAULT_IP = "169.254.169.254/32" +OS_FLOATING_IP_RULE_KEY = 'OS_FLOATING_IP' +OS_SG_RULE_KEY = 'OS_SG_RULE_ID' +OS_TENANT_ROUTER_RULE_KEY = 'OS_TENANT_ROUTER_RULE' +PRE_ROUTING_CHAIN_NAME = "OS_PRE_ROUTING_%s" +PORT_INBOUND_CHAIN_NAME = "OS_PORT_%s_INBOUND" +PORT_OUTBOUND_CHAIN_NAME = "OS_PORT_%s_OUTBOUND" +POST_ROUTING_CHAIN_NAME = "OS_POST_ROUTING_%s" +SG_INGRESS_CHAIN_NAME = "OS_SG_%s_INGRESS" +SG_EGRESS_CHAIN_NAME = "OS_SG_%s_EGRESS" +SG_PORT_GROUP_NAME = "OS_PG_%s" +SNAT_RULE = 'SNAT' -class MidonetPluginException(q_exc.NeutronException): + +def _get_nat_ips(type, fip): + """Get NAT IP address information. + + From the route type given, determine the source and target IP addresses + from the provided floating IP DB object. + """ + if type == 'pre-routing': + return fip["floating_ip_address"], fip["fixed_ip_address"] + elif type == 'post-routing': + return fip["fixed_ip_address"], fip["floating_ip_address"] + else: + raise ValueError(_("Invalid nat_type %s") % type) + + +def _nat_chain_names(router_id): + """Get the chain names for NAT. + + These names are used to associate MidoNet chains to the NAT rules + applied to the router. For each of these, there are two NAT types, + 'dnat' and 'snat' that are returned as keys, and the corresponding + chain names as their values. + """ + pre_routing_name = PRE_ROUTING_CHAIN_NAME % router_id + post_routing_name = POST_ROUTING_CHAIN_NAME % router_id + return {'pre-routing': pre_routing_name, 'post-routing': post_routing_name} + + +def _sg_chain_names(sg_id): + """Get the chain names for security group. + + These names are used to associate a security group to MidoNet chains. + There are two names for ingress and egress security group directions. + """ + ingress = SG_INGRESS_CHAIN_NAME % sg_id + egress = SG_EGRESS_CHAIN_NAME % sg_id + return {'ingress': ingress, 'egress': egress} + + +def _port_chain_names(port_id): + """Get the chain names for a port. + + These are chains to hold security group chains. + """ + inbound = PORT_INBOUND_CHAIN_NAME % port_id + outbound = PORT_OUTBOUND_CHAIN_NAME % port_id + return {'inbound': inbound, 'outbound': outbound} + + +def _sg_port_group_name(sg_id): + """Get the port group name for security group.. + + This name is used to associate a security group to MidoNet port groups. + """ + return SG_PORT_GROUP_NAME % sg_id + + +def _rule_direction(sg_direction): + """Convert the SG direction to MidoNet direction + + MidoNet terms them 'inbound' and 'outbound' instead of 'ingress' and + 'egress'. Also, the direction is reversed since MidoNet sees it + from the network port's point of view, not the VM's. + """ + if sg_direction == 'ingress': + return 'outbound' + elif sg_direction == 'egress': + return 'inbound' + else: + raise ValueError(_("Unrecognized direction %s") % sg_direction) + + +def _is_router_interface_port(port): + """Check whether the given port is a router interface port.""" + device_owner = port['device_owner'] + return (device_owner in l3_db.DEVICE_OWNER_ROUTER_INTF) + + +def _is_router_gw_port(port): + """Check whether the given port is a router gateway port.""" + device_owner = port['device_owner'] + return (device_owner in l3_db.DEVICE_OWNER_ROUTER_GW) + + +def _is_vif_port(port): + """Check whether the given port is a standard VIF port.""" + device_owner = port['device_owner'] + return (not _is_dhcp_port(port) and + device_owner not in (l3_db.DEVICE_OWNER_ROUTER_GW, + l3_db.DEVICE_OWNER_ROUTER_INTF)) + + +def _is_dhcp_port(port): + """Check whether the given port is a DHCP port.""" + device_owner = port['device_owner'] + return device_owner.startswith('network:dhcp') + + +def _check_resource_exists(func, id, name, raise_exc=False): + """Check whether the given resource exists in MidoNet data store.""" + try: + func(id) + except midonet_lib.MidonetResourceNotFound as exc: + LOG.error(_("There is no %(name)s with ID %(id)s in MidoNet."), + {"name": name, "id": id}) + if raise_exc: + raise MidonetPluginException(msg=exc) + + +class MidoRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin): + RPC_API_VERSION = '1.1' + + def create_rpc_dispatcher(self): + """Get the rpc dispatcher for this manager. + + This a basic implementation that will call the plugin like get_ports + and handle basic events + If a manager would like to set an rpc API version, or support more than + one class as the target of rpc messages, override this method. + """ + return n_rpc.PluginRpcDispatcher([self, + agents_db.AgentExtRpcCallback()]) + + +class MidonetPluginException(n_exc.NeutronException): message = _("%(msg)s") class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, l3_db.L3_NAT_db_mixin, + agentschedulers_db.AgentSchedulerDbMixin, securitygroups_db.SecurityGroupDbMixin): - supported_extension_aliases = ['router', 'security-group'] + supported_extension_aliases = ['router', 'security-group', 'agent' + 'dhcp_agent_scheduler'] __native_bulk_support = False def __init__(self): - # Read config values midonet_conf = cfg.CONF.MIDONET midonet_uri = midonet_conf.midonet_uri admin_user = midonet_conf.username admin_pass = midonet_conf.password admin_project_id = midonet_conf.project_id - provider_router_id = midonet_conf.provider_router_id - metadata_router_id = midonet_conf.metadata_router_id - mode = midonet_conf.mode + self.provider_router_id = midonet_conf.provider_router_id + self.provider_router = None self.mido_api = api.MidonetApi(midonet_uri, admin_user, admin_pass, project_id=admin_project_id) self.client = midonet_lib.MidoClient(self.mido_api) - if provider_router_id and metadata_router_id: - # get MidoNet provider router and metadata router - self.provider_router = self.client.get_router(provider_router_id) - self.metadata_router = self.client.get_router(metadata_router_id) - - elif not provider_router_id or not metadata_router_id: - if mode == 'dev': - msg = _('No provider router and metadata device ids found. ' - 'But skipping because running in dev env.') - LOG.debug(msg) - else: - msg = _('provider_router_id and metadata_router_id ' - 'should be configured in the plugin config file') - LOG.exception(msg) - raise MidonetPluginException(msg=msg) + # self.provider_router_id should have been set. + if self.provider_router_id is None: + msg = _('provider_router_id should be configured in the plugin ' + 'config file') + LOG.exception(msg) + raise MidonetPluginException(msg=msg) + self.setup_rpc() db.configure_db() + def _get_provider_router(self): + if self.provider_router is None: + self.provider_router = self.client.get_router( + self.provider_router_id) + return self.provider_router + + def _dhcp_mappings(self, context, fixed_ips, mac): + for fixed_ip in fixed_ips: + subnet = self._get_subnet(context, fixed_ip["subnet_id"]) + if subnet["ip_version"] == 6: + # TODO(ryu) handle IPv6 + continue + yield subnet['cidr'], fixed_ip["ip_address"], mac + + def _metadata_subnets(self, context, fixed_ips): + for fixed_ip in fixed_ips: + subnet = self._get_subnet(context, fixed_ip["subnet_id"]) + if subnet["ip_version"] == 6: + continue + yield subnet['cidr'], fixed_ip["ip_address"] + + def _initialize_port_chains(self, port, in_chain, out_chain, sg_ids): + + tenant_id = port["tenant_id"] + + position = 1 + # mac spoofing protection + self._add_chain_rule(in_chain, action='drop', + dl_src=port["mac_address"], inv_dl_src=True, + position=position) + + # ip spoofing protection + for fixed_ip in port["fixed_ips"]: + position += 1 + self._add_chain_rule(in_chain, action="drop", + src_addr=fixed_ip["ip_address"] + "/32", + inv_nw_src=True, dl_type=0x0800, # IPv4 + position=position) + + # conntrack + position += 1 + self._add_chain_rule(in_chain, action='accept', + match_forward_flow=True, + position=position) + + # Reset the position to process egress + position = 1 + + # Add rule for SGs + if sg_ids: + for sg_id in sg_ids: + chain_name = _sg_chain_names(sg_id)["ingress"] + chain = self.client.get_chain_by_name(tenant_id, chain_name) + self._add_chain_rule(out_chain, action='jump', + jump_chain_id=chain.get_id(), + jump_chain_name=chain_name, + position=position) + position += 1 + + # add reverse flow matching at the end + self._add_chain_rule(out_chain, action='accept', + match_return_flow=True, + position=position) + position += 1 + + # fall back DROP rule at the end except for ARP + self._add_chain_rule(out_chain, action='drop', + dl_type=0x0806, # ARP + inv_dl_type=True, position=position) + + def _bind_port_to_sgs(self, context, port, sg_ids): + self._process_port_create_security_group(context, port, sg_ids) + for sg_id in sg_ids: + pg_name = _sg_port_group_name(sg_id) + self.client.add_port_to_port_group_by_name(port["tenant_id"], + pg_name, port["id"]) + + def _unbind_port_from_sgs(self, context, port_id): + self._delete_port_security_group_bindings(context, port_id) + self.client.remove_port_from_port_groups(port_id) + + def _create_accept_chain_rule(self, context, sg_rule, chain=None): + direction = sg_rule["direction"] + tenant_id = sg_rule["tenant_id"] + sg_id = sg_rule["security_group_id"] + chain_name = _sg_chain_names(sg_id)[direction] + + if chain is None: + chain = self.client.get_chain_by_name(tenant_id, chain_name) + + pg_id = None + if sg_rule["remote_group_id"] is not None: + pg_name = _sg_port_group_name(sg_id) + pg = self.client.get_port_group_by_name(tenant_id, pg_name) + pg_id = pg.get_id() + + props = {OS_SG_RULE_KEY: str(sg_rule["id"])} + + # Determine source or destination address by looking at direction + src_pg_id = dst_pg_id = None + src_addr = dst_addr = None + src_port_to = dst_port_to = None + src_port_from = dst_port_from = None + if direction == "egress": + dst_pg_id = pg_id + dst_addr = sg_rule["remote_ip_prefix"] + dst_port_from = sg_rule["port_range_min"] + dst_port_to = sg_rule["port_range_max"] + else: + src_pg_id = pg_id + src_addr = sg_rule["remote_ip_prefix"] + src_port_from = sg_rule["port_range_min"] + src_port_to = sg_rule["port_range_max"] + + return self._add_chain_rule( + chain, action='accept', port_group_src=src_pg_id, + port_group_dst=dst_pg_id, + src_addr=src_addr, src_port_from=src_port_from, + src_port_to=src_port_to, + dst_addr=dst_addr, dst_port_from=dst_port_from, + dst_port_to=dst_port_to, + nw_proto=net_util.get_protocol_value(sg_rule["protocol"]), + dl_type=net_util.get_ethertype_value(sg_rule["ethertype"]), + properties=props) + + def setup_rpc(self): + # RPC support + self.topic = topics.PLUGIN + self.conn = rpc.create_connection(new=True) + self.callbacks = MidoRpcCallbacks() + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, + fanout=False) + # Consume from all consumers in a thread + self.conn.consume_in_thread() + def create_subnet(self, context, subnet): """Create Neutron subnet. @@ -91,16 +364,9 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, """ LOG.debug(_("MidonetPluginV2.create_subnet called: subnet=%r"), subnet) - if subnet['subnet']['ip_version'] == 6: - raise q_exc.NotImplementedError( - _("MidoNet doesn't support IPv6.")) - + subnet_data = subnet["subnet"] net = super(MidonetPluginV2, self).get_network( context, subnet['subnet']['network_id'], fields=None) - if net['subnets']: - raise q_exc.NotImplementedError( - _("MidoNet doesn't support multiple subnets " - "on the same network.")) session = context.session with session.begin(subtransactions=True): @@ -108,19 +374,14 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, subnet) bridge = self.client.get_bridge(sn_entry['network_id']) - gateway_ip = subnet['subnet']['gateway_ip'] - network_address, prefix = subnet['subnet']['cidr'].split('/') - self.client.create_dhcp(bridge, gateway_ip, network_address, - prefix) + gateway_ip = subnet_data['gateway_ip'] + cidr = subnet_data['cidr'] + self.client.create_dhcp(bridge, gateway_ip, cidr) # For external network, link the bridge to the provider router. if net['router:external']: - gateway_ip = sn_entry['gateway_ip'] - network_address, length = sn_entry['cidr'].split('/') - - self.client.link_bridge_to_provider_router( - bridge, self.provider_router, gateway_ip, network_address, - length) + self._link_bridge_to_gw_router( + bridge, self._get_provider_router(), gateway_ip, cidr) LOG.debug(_("MidonetPluginV2.create_subnet exiting: sn_entry=%r"), sn_entry) @@ -142,8 +403,8 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, # If the network is external, clean up routes, links, ports. if net['router:external']: - self.client.unlink_bridge_from_provider_router( - bridge, self.provider_router) + self._unlink_bridge_from_gw_router(bridge, + self._get_provider_router()) super(MidonetPluginV2, self).delete_subnet(context, id) LOG.debug(_("MidonetPluginV2.delete_subnet exiting")) @@ -155,26 +416,18 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, """ LOG.debug(_('MidonetPluginV2.create_network called: network=%r'), network) - - if network['network']['admin_state_up'] is False: - LOG.warning(_('Ignoring admin_state_up=False for network=%r ' - 'because it is not yet supported'), network) - tenant_id = self._get_tenant_id_for_create(context, network['network']) - self._ensure_default_security_group(context, tenant_id) + bridge = self.client.create_bridge(tenant_id, + network['network']['name']) + network['network']['id'] = bridge.get_id() + session = context.session with session.begin(subtransactions=True): - bridge = self.client.create_bridge(tenant_id, - network['network']['name']) - - # Set MidoNet bridge ID to the neutron DB entry - network['network']['id'] = bridge.get_id() net = super(MidonetPluginV2, self).create_network(context, network) - - # to handle l3 related data in DB self._process_l3_create(context, net, network['network']) + LOG.debug(_("MidonetPluginV2.create_network exiting: net=%r"), net) return net @@ -186,14 +439,6 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, """ LOG.debug(_("MidonetPluginV2.update_network called: id=%(id)r, " "network=%(network)r"), {'id': id, 'network': network}) - - # Reject admin_state_up=False - if network['network'].get('admin_state_up') and network['network'][ - 'admin_state_up'] is False: - raise q_exc.NotImplementedError(_('admin_state_up=False ' - 'networks are not ' - 'supported.')) - session = context.session with session.begin(subtransactions=True): net = super(MidonetPluginV2, self).update_network( @@ -210,7 +455,6 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, """ LOG.debug(_("MidonetPluginV2.get_network called: id=%(id)r, " "fields=%(fields)r"), {'id': id, 'fields': fields}) - qnet = super(MidonetPluginV2, self).get_network(context, id, fields) self.client.get_bridge(id) @@ -232,77 +476,89 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, """Create a L2 port in Neutron/MidoNet.""" LOG.debug(_("MidonetPluginV2.create_port called: port=%r"), port) - is_compute_interface = False port_data = port['port'] - # get the bridge and create a port on it. - bridge = self.client.get_bridge(port_data['network_id']) - device_owner = port_data['device_owner'] + # Create a bridge port in MidoNet and set the bridge port ID as the + # port ID in Neutron. + bridge = self.client.get_bridge(port_data["network_id"]) + tenant_id = bridge.get_tenant_id() + bridge_port = self.client.add_bridge_port(bridge) + port_data["id"] = bridge_port.get_id() + try: + session = context.session + with session.begin(subtransactions=True): + # Create a Neutron port + new_port = super(MidonetPluginV2, self).create_port(context, + port) + port_data.update(new_port) - if device_owner.startswith('compute:') or device_owner is '': - is_compute_interface = True - bridge_port = self.client.create_exterior_bridge_port(bridge) - elif device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF: - bridge_port = self.client.create_interior_bridge_port(bridge) - elif (device_owner == l3_db.DEVICE_OWNER_ROUTER_GW or - device_owner == l3_db.DEVICE_OWNER_FLOATINGIP): + # Bind security groups to the port + sg_ids = self._get_security_groups_on_port(context, port) + if sg_ids: + self._bind_port_to_sgs(context, port_data, sg_ids) + port_data[ext_sg.SECURITYGROUPS] = sg_ids - # This is a dummy port to make l3_db happy. - # This will not be used in MidoNet - bridge_port = self.client.create_interior_bridge_port(bridge) + # Create port chains + port_chains = {} + for d, name in _port_chain_names(new_port["id"]).iteritems(): + port_chains[d] = self.client.create_chain(tenant_id, name) - if bridge_port: - # set midonet port id to neutron port id and create a DB record. - port_data['id'] = bridge_port.get_id() + self._initialize_port_chains(port_data, port_chains['inbound'], + port_chains['outbound'], sg_ids) - session = context.session - with session.begin(subtransactions=True): - port_db_entry = super(MidonetPluginV2, - self).create_port(context, port) - # Caveat: port_db_entry is not a db model instance - sg_ids = self._get_security_groups_on_port(context, port) - self._process_port_create_security_group(context, port, sg_ids) - if is_compute_interface: - # Create a DHCP entry if needed. - if 'ip_address' in (port_db_entry['fixed_ips'] or [{}])[0]: - # get ip and mac from DB record, assuming one IP address - # at most since we only support one subnet per network now. - fixed_ip = port_db_entry['fixed_ips'][0]['ip_address'] - mac = port_db_entry['mac_address'] - # create dhcp host entry under the bridge. - self.client.create_dhcp_hosts(bridge, fixed_ip, mac) - LOG.debug(_("MidonetPluginV2.create_port exiting: port_db_entry=%r"), - port_db_entry) - return port_db_entry + # Update the port with the chain + self.client.update_port_chains( + bridge_port, port_chains["inbound"].get_id(), + port_chains["outbound"].get_id()) + + if _is_dhcp_port(port_data): + # For DHCP port, add a metadata route + for cidr, ip in self._metadata_subnets( + context, port_data["fixed_ips"]): + self.client.add_dhcp_route_option(bridge, cidr, ip, + METADATA_DEFAULT_IP) + elif _is_vif_port(port_data): + # DHCP mapping is only for VIF ports + for cidr, ip, mac in self._dhcp_mappings( + context, port_data["fixed_ips"], + port_data["mac_address"]): + self.client.add_dhcp_host(bridge, cidr, ip, mac) + + except Exception as ex: + # Try removing the MidoNet port before raising an exception. + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed to create a port on network %(net_id)s: " + "%(err)s"), + {"net_id": port_data["network_id"], "err": ex}) + self.client.delete_port(bridge_port.get_id()) + + LOG.debug(_("MidonetPluginV2.create_port exiting: port=%r"), port_data) + return port_data def get_port(self, context, id, fields=None): """Retrieve port.""" LOG.debug(_("MidonetPluginV2.get_port called: id=%(id)s " "fields=%(fields)r"), {'id': id, 'fields': fields}) - - # get the neutron port from DB. - port_db_entry = super(MidonetPluginV2, self).get_port(context, - id, fields) - # verify that corresponding port exists in MidoNet. - self.client.get_port(id) - - LOG.debug(_("MidonetPluginV2.get_port exiting: port_db_entry=%r"), - port_db_entry) - return port_db_entry + port = super(MidonetPluginV2, self).get_port(context, id, fields) + "Check if the port exists in MidoNet DB""" + try: + self.client.get_port(id) + except midonet_lib.MidonetResourceNotFound as exc: + LOG.error(_("There is no port with ID %(id)s in MidoNet."), + {"id": id}) + port['status'] = constants.PORT_STATUS_ERROR + raise exc + LOG.debug(_("MidonetPluginV2.get_port exiting: port=%r"), port) + return port def get_ports(self, context, filters=None, fields=None): """List neutron ports and verify that they exist in MidoNet.""" LOG.debug(_("MidonetPluginV2.get_ports called: filters=%(filters)s " "fields=%(fields)r"), {'filters': filters, 'fields': fields}) - ports_db_entry = super(MidonetPluginV2, self).get_ports(context, - filters, - fields) - if ports_db_entry: - for port in ports_db_entry: - if 'security_gorups' in port: - self._extend_port_dict_security_group(context, port) - return ports_db_entry + ports = super(MidonetPluginV2, self).get_ports(context, filters, + fields) + return ports def delete_port(self, context, id, l3_port_check=True): """Delete a neutron port and corresponding MidoNet bridge port.""" @@ -314,202 +570,382 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, if l3_port_check: self.prevent_l3_port_deletion(context, id) - session = context.session - with session.begin(subtransactions=True): - port_db_entry = super(MidonetPluginV2, self).get_port(context, - id, None) - # Clean up dhcp host entry if needed. - if 'ip_address' in (port_db_entry['fixed_ips'] or [{}])[0]: - # get ip and mac from DB record. - ip = port_db_entry['fixed_ips'][0]['ip_address'] - mac = port_db_entry['mac_address'] + port = self.get_port(context, id) + device_id = port['device_id'] + # If this port is for router interface/gw, unlink and delete. + if _is_router_interface_port(port): + self._unlink_bridge_from_router(device_id, id) + elif _is_router_gw_port(port): + # Gateway removed + # Remove all the SNAT rules that are tagged. + router = self._get_router(context, device_id) + tenant_id = router["tenant_id"] + chain_names = _nat_chain_names(device_id) + for _type, name in chain_names.iteritems(): + self.client.remove_rules_by_property( + tenant_id, name, OS_TENANT_ROUTER_RULE_KEY, + SNAT_RULE) + # Remove the default routes and unlink + self._remove_router_gateway(port['device_id']) - # create dhcp host entry under the bridge. - self.client.delete_dhcp_hosts(port_db_entry['network_id'], ip, - mac) + self.client.delete_port(id, delete_chains=True) + try: + for cidr, ip, mac in self._dhcp_mappings( + context, port["fixed_ips"], port["mac_address"]): + self.client.delete_dhcp_host(port["network_id"], cidr, ip, + mac) + except Exception: + LOG.error(_("Failed to delete DHCP mapping for port %(id)s"), + {"id": id}) - self.client.delete_port(id) - return super(MidonetPluginV2, self).delete_port(context, id) + super(MidonetPluginV2, self).delete_port(context, id) - # - # L3 APIs. - # + def update_port(self, context, id, port): + """Handle port update, including security groups and fixed IPs.""" + with context.session.begin(subtransactions=True): + + # Get the port and save the fixed IPs + old_port = self._get_port(context, id) + net_id = old_port["network_id"] + mac = old_port["mac_address"] + old_fixed_ips = old_port.get('fixed_ips') + + # update the port DB + p = super(MidonetPluginV2, self).update_port(context, id, port) + + if "fixed_ips" in p: + # IPs have changed. Re-map the DHCP entries + bridge = self.client.get_bridge(net_id) + for cidr, ip, mac in self._dhcp_mappings( + context, old_fixed_ips, mac): + self.client.remove_dhcp_host(bridge, cidr, ip, mac) + for cidr, ip, mac in self._dhcp_mappings(context, + p["fixed_ips"], mac): + self.client.add_dhcp_host(bridge, cidr, ip, mac) + + if (self._check_update_deletes_security_groups(port) or + self._check_update_has_security_groups(port)): + self._unbind_port_from_sgs(context, p["id"]) + sg_ids = self._get_security_groups_on_port(context, port) + if sg_ids: + self._bind_port_to_sgs(context, p, sg_ids) + return p def create_router(self, context, router): - LOG.debug(_("MidonetPluginV2.create_router called: router=%r"), router) + """Handle router creation. - if router['router']['admin_state_up'] is False: - LOG.warning(_('Ignoring admin_state_up=False for router=%r. ' - 'Overriding with True'), router) - router['router']['admin_state_up'] = True + When a new Neutron router is created, its corresponding MidoNet router + is also created. In MidoNet, this router is initialized with chains + for inbuond and outbound traffic, which will be used to hold other + chains that include various rules, such as NAT. + :param router: Router information provided to create a new router. + """ + LOG.debug(_("MidonetPluginV2.create_router called: router=%(router)s"), + {"router": router}) tenant_id = self._get_tenant_id_for_create(context, router['router']) - session = context.session - with session.begin(subtransactions=True): - mrouter = self.client.create_tenant_router( - tenant_id, router['router']['name'], self.metadata_router) + mido_router = self.client.create_router(tenant_id, + router['router']['name']) + mido_router_id = mido_router.get_id() - qrouter = super(MidonetPluginV2, self).create_router(context, - router) + try: + with context.session.begin(subtransactions=True): - # get entry from the DB and update 'id' with MidoNet router id. - qrouter_entry = self._get_router(context, qrouter['id']) - qrouter['id'] = mrouter.get_id() - qrouter_entry.update(qrouter) + router_data = super(MidonetPluginV2, self).create_router( + context, router) - LOG.debug(_("MidonetPluginV2.create_router exiting: qrouter=%r"), - qrouter) - return qrouter + # get entry from the DB and update 'id' with MidoNet router id. + router_db = self._get_router(context, router_data['id']) + router_data['id'] = mido_router_id + router_db.update(router_data) + except Exception: + # Try removing the midonet router + with excutils.save_and_reraise_exception(): + self.client.delete_router(mido_router_id) + + # Create router chains + chain_names = _nat_chain_names(mido_router_id) + try: + self.client.add_router_chains(mido_router, + chain_names["pre-routing"], + chain_names["post-routing"]) + except Exception: + # Set the router status to Error + with context.session.begin(subtransactions=True): + r = self._get_router(context, router_data["id"]) + router_data['status'] = constants.NET_STATUS_ERROR + r['status'] = router_data['status'] + context.session.add(r) + + LOG.debug(_("MidonetPluginV2.create_router exiting: " + "router_data=%(router_data)s."), + {"router_data": router_data}) + return router_data + + def _set_router_gateway(self, id, gw_router, gw_ip): + """Set router uplink gateway + + :param ID: ID of the router + :param gw_router: gateway router to link to + :param gw_ip: gateway IP address + """ + LOG.debug(_("MidonetPluginV2.set_router_gateway called: id=%(id)s, " + "gw_router=%(gw_router)s, gw_ip=%(gw_ip)s"), + {'id': id, 'gw_router': gw_router, 'gw_ip': gw_ip}), + + router = self.client.get_router(id) + + # Create a port in the gw router + gw_port = self.client.add_router_port(gw_router, + port_address='169.254.255.1', + network_address='169.254.255.0', + network_length=30) + + # Create a port in the router + port = self.client.add_router_port(router, + port_address='169.254.255.2', + network_address='169.254.255.0', + network_length=30) + + # Link them + self.client.link(gw_port, port.get_id()) + + # Add a route for gw_ip to bring it down to the router + self.client.add_router_route(gw_router, type='Normal', + src_network_addr='0.0.0.0', + src_network_length=0, + dst_network_addr=gw_ip, + dst_network_length=32, + next_hop_port=gw_port.get_id(), + weight=100) + + # Add default route to uplink in the router + self.client.add_router_route(router, type='Normal', + src_network_addr='0.0.0.0', + src_network_length=0, + dst_network_addr='0.0.0.0', + dst_network_length=0, + next_hop_port=port.get_id(), + weight=100) + + def _remove_router_gateway(self, id): + """Clear router gateway + + :param ID: ID of the router + """ + LOG.debug(_("MidonetPluginV2.remove_router_gateway called: " + "id=%(id)s"), {'id': id}) + router = self.client.get_router(id) + + # delete the port that is connected to the gateway router + for p in router.get_ports(): + if p.get_port_address() == '169.254.255.2': + peer_port_id = p.get_peer_id() + if peer_port_id is not None: + self.client.unlink(p) + self.client.delete_port(peer_port_id) + + # delete default route + for r in router.get_routes(): + if (r.get_dst_network_addr() == '0.0.0.0' and + r.get_dst_network_length() == 0): + self.client.delete_route(r.get_id()) def update_router(self, context, id, router): + """Handle router updates.""" LOG.debug(_("MidonetPluginV2.update_router called: id=%(id)s " - "router=%(router)r"), router) + "router=%(router)r"), {"id": id, "router": router}) - if router['router'].get('admin_state_up') is False: - raise q_exc.NotImplementedError(_('admin_state_up=False ' - 'routers are not ' - 'supported.')) + router_data = router["router"] - op_gateway_set = False - op_gateway_clear = False + # Check if the update included changes to the gateway. + gw_updated = l3_db.EXTERNAL_GW_INFO in router_data + with context.session.begin(subtransactions=True): - # figure out which operation it is in - if ('external_gateway_info' in router['router'] and - 'network_id' in router['router']['external_gateway_info']): - op_gateway_set = True - elif ('external_gateway_info' in router['router'] and - router['router']['external_gateway_info'] == {}): - op_gateway_clear = True + # Update the Neutron DB + r = super(MidonetPluginV2, self).update_router(context, id, + router) + tenant_id = r["tenant_id"] + if gw_updated: + if (l3_db.EXTERNAL_GW_INFO in r and + r[l3_db.EXTERNAL_GW_INFO] is not None): + # Gateway created + gw_port = self._get_port(context, r["gw_port_id"]) + gw_ip = gw_port['fixed_ips'][0]['ip_address'] - qports = super(MidonetPluginV2, self).get_ports( - context, {'device_id': [id], - 'device_owner': ['network:router_gateway']}) + # First link routers and set up the routes + self._set_router_gateway(r["id"], + self._get_provider_router(), + gw_ip) - assert len(qports) == 1 - qport = qports[0] - snat_ip = qport['fixed_ips'][0]['ip_address'] - qport['network_id'] + # Get the NAT chains and add dynamic SNAT rules. + chain_names = _nat_chain_names(r["id"]) + props = {OS_TENANT_ROUTER_RULE_KEY: SNAT_RULE} + self.client.add_dynamic_snat(tenant_id, + chain_names['pre-routing'], + chain_names['post-routing'], + gw_ip, gw_port["id"], **props) - session = context.session - with session.begin(subtransactions=True): - - qrouter = super(MidonetPluginV2, self).update_router(context, id, - router) - - changed_name = router['router'].get('name') + # Update the name if changed + changed_name = router_data.get('name') if changed_name: self.client.update_router(id, changed_name) - if op_gateway_set: - # find a qport with the network_id for the router - qports = super(MidonetPluginV2, self).get_ports( - context, {'device_id': [id], - 'device_owner': ['network:router_gateway']}) - assert len(qports) == 1 - qport = qports[0] - snat_ip = qport['fixed_ips'][0]['ip_address'] - - self.client.set_router_external_gateway(id, - self.provider_router, - snat_ip) - - if op_gateway_clear: - self.client.clear_router_external_gateway(id) - - LOG.debug(_("MidonetPluginV2.update_router exiting: qrouter=%r"), - qrouter) - return qrouter + LOG.debug(_("MidonetPluginV2.update_router exiting: router=%r"), r) + return r def delete_router(self, context, id): + """Handler for router deletion. + + Deleting a router on Neutron simply means deleting its corresponding + router in MidoNet. + + :param id: router ID to remove + """ LOG.debug(_("MidonetPluginV2.delete_router called: id=%s"), id) - self.client.delete_tenant_router(id) + self.client.delete_router_chains(id) + self.client.delete_router(id) - result = super(MidonetPluginV2, self).delete_router(context, id) - LOG.debug(_("MidonetPluginV2.delete_router exiting: result=%s"), - result) - return result + super(MidonetPluginV2, self).delete_router(context, id) + + def _link_bridge_to_gw_router(self, bridge, gw_router, gw_ip, cidr): + """Link a bridge to the gateway router + + :param bridge: bridge + :param gw_router: gateway router to link to + :param gw_ip: IP address of gateway + :param cidr: network CIDR + """ + net_addr, net_len = net_util.net_addr(cidr) + + # create a port on the gateway router + gw_port = self.client.add_router_port(gw_router, port_address=gw_ip, + network_address=net_addr, + network_length=net_len) + + # create a bridge port, then link it to the router. + port = self.client.add_bridge_port(bridge) + self.client.link(gw_port, port.get_id()) + + # add a route for the subnet in the gateway router + self.client.add_router_route(gw_router, type='Normal', + src_network_addr='0.0.0.0', + src_network_length=0, + dst_network_addr=net_addr, + dst_network_length=net_len, + next_hop_port=gw_port.get_id(), + weight=100) + + def _unlink_bridge_from_gw_router(self, bridge, gw_router): + """Unlink a bridge from the gateway router + + :param bridge: bridge to unlink + :param gw_router: gateway router to unlink from + """ + # Delete routes and unlink the router and the bridge. + routes = self.client.get_router_routes(gw_router.get_id()) + + bridge_ports_to_delete = [ + p for p in gw_router.get_peer_ports() + if p.get_device_id() == bridge.get_id()] + + for p in bridge.get_peer_ports(): + if p.get_device_id() == gw_router.get_id(): + # delete the routes going to the bridge + for r in routes: + if r.get_next_hop_port() == p.get_id(): + self.client.delete_route(r.get_id()) + self.client.unlink(p) + self.client.delete_port(p.get_id()) + + # delete bridge port + for port in bridge_ports_to_delete: + self.client.delete_port(port.get_id()) + + def _link_bridge_to_router(self, router, bridge_port_id, net_addr, net_len, + gw_ip, metadata_gw_ip): + router_port = self.client.add_router_port( + router, port_address=gw_ip, network_address=net_addr, + network_length=net_len) + self.client.link(router_port, bridge_port_id) + self.client.add_router_route(router, type='Normal', + src_network_addr='0.0.0.0', + src_network_length=0, + dst_network_addr=net_addr, + dst_network_length=net_len, + next_hop_port=router_port.get_id(), + weight=100) + + if metadata_gw_ip: + # Add a route for the metadata server. + # Not all VM images supports DHCP option 121. Add a route for the + # Metadata server in the router to forward the packet to the bridge + # that will send them to the Metadata Proxy. + net_addr, net_len = net_util.net_addr(METADATA_DEFAULT_IP) + self.client.add_router_route( + router, type='Normal', src_network_addr=net_addr, + src_network_length=net_len, + dst_network_addr=net_addr, + dst_network_length=32, + next_hop_port=router_port.get_id(), + next_hop_gateway=metadata_gw_ip) + + def _unlink_bridge_from_router(self, router_id, bridge_port_id): + """Unlink a bridge from a router.""" + + # Remove the routes to the port and unlink the port + bridge_port = self.client.get_port(bridge_port_id) + routes = self.client.get_router_routes(router_id) + self.client.delete_port_routes(routes, bridge_port.get_peer_id()) + self.client.unlink(bridge_port) def add_router_interface(self, context, router_id, interface_info): + """Handle router linking with network.""" LOG.debug(_("MidonetPluginV2.add_router_interface called: " "router_id=%(router_id)s " "interface_info=%(interface_info)r"), {'router_id': router_id, 'interface_info': interface_info}) - qport = super(MidonetPluginV2, self).add_router_interface( - context, router_id, interface_info) + with context.session.begin(subtransactions=True): + info = super(MidonetPluginV2, self).add_router_interface( + context, router_id, interface_info) - # TODO(tomoe): handle a case with 'port' in interface_info - if 'subnet_id' in interface_info: - subnet_id = interface_info['subnet_id'] - subnet = self._get_subnet(context, subnet_id) + try: + subnet = self._get_subnet(context, info["subnet_id"]) + cidr = subnet["cidr"] + net_addr, net_len = net_util.net_addr(cidr) + router = self.client.get_router(router_id) - gateway_ip = subnet['gateway_ip'] - network_address, length = subnet['cidr'].split('/') - - # Link the router and the bridge port. - self.client.link_bridge_port_to_router(qport['port_id'], router_id, - gateway_ip, network_address, - length, - self.metadata_router) + # Get the metadatat GW IP + metadata_gw_ip = None + rport_qry = context.session.query(models_v2.Port) + dhcp_ports = rport_qry.filter_by( + network_id=subnet["network_id"], + device_owner='network:dhcp').all() + if dhcp_ports and dhcp_ports[0].fixed_ips: + metadata_gw_ip = dhcp_ports[0].fixed_ips[0].ip_address + else: + LOG.warn(_("DHCP agent is not working correctly. No port " + "to reach the Metadata server on this network")) + # Link the router and the bridge + self._link_bridge_to_router(router, info["port_id"], net_addr, + net_len, subnet["gateway_ip"], + metadata_gw_ip) + except Exception: + LOG.error(_("Failed to create MidoNet resources to add router " + "interface. info=%(info)s, router_id=%(router_id)s"), + {"info": info, "router_id": router_id}) + with excutils.save_and_reraise_exception(): + with context.session.begin(subtransactions=True): + self.remove_router_interface(context, router_id, info) LOG.debug(_("MidonetPluginV2.add_router_interface exiting: " - "qport=%r"), qport) - return qport - - def remove_router_interface(self, context, router_id, interface_info): - """Remove interior router ports.""" - LOG.debug(_("MidonetPluginV2.remove_router_interface called: " - "router_id=%(router_id)s " - "interface_info=%(interface_info)r"), - {'router_id': router_id, 'interface_info': interface_info}) - port_id = None - if 'port_id' in interface_info: - - port_id = interface_info['port_id'] - subnet_id = self.get_port(context, - interface_info['port_id'] - )['fixed_ips'][0]['subnet_id'] - - subnet = self._get_subnet(context, subnet_id) - - if 'subnet_id' in interface_info: - - subnet_id = interface_info['subnet_id'] - subnet = self._get_subnet(context, subnet_id) - network_id = subnet['network_id'] - - # find a neutron port for the network - rport_qry = context.session.query(models_v2.Port) - ports = rport_qry.filter_by( - device_id=router_id, - device_owner=l3_db.DEVICE_OWNER_ROUTER_INTF, - network_id=network_id) - network_port = None - for p in ports: - if p['fixed_ips'][0]['subnet_id'] == subnet_id: - network_port = p - break - assert network_port - port_id = network_port['id'] - - assert port_id - - # get network information from subnet data - network_addr, network_length = subnet['cidr'].split('/') - network_length = int(network_length) - - # Unlink the router and the bridge. - self.client.unlink_bridge_port_from_router(port_id, network_addr, - network_length, - self.metadata_router) - - info = super(MidonetPluginV2, self).remove_router_interface( - context, router_id, interface_info) - LOG.debug(_("MidonetPluginV2.remove_router_interface exiting")) + "info=%r"), info) return info def update_floatingip(self, context, id, floatingip): + """Handle floating IP assocation and disassociation.""" LOG.debug(_("MidonetPluginV2.update_floatingip called: id=%(id)s " "floatingip=%(floatingip)s "), {'id': id, 'floatingip': floatingip}) @@ -520,29 +956,60 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, fip = super(MidonetPluginV2, self).update_floatingip( context, id, floatingip) - self.client.setup_floating_ip(fip['router_id'], - self.provider_router, - fip['floating_ip_address'], - fip['fixed_ip_address'], id) + # Add a route for the floating IP on the provider router. + router = self.client.get_router(fip["router_id"]) + link_port = self.client.get_link_port( + self._get_provider_router(), router.get_id()) + self.client.add_router_route( + self._get_provider_router(), + src_network_addr='0.0.0.0', + src_network_length=0, + dst_network_addr=fip["floating_ip_address"], + dst_network_length=32, + next_hop_port=link_port.get_peer_id()) + + # Add static SNAT and DNAT rules on the tenant router. + props = {OS_FLOATING_IP_RULE_KEY: id} + tenant_id = router.get_tenant_id() + chain_names = _nat_chain_names(router.get_id()) + for chain_type, name in chain_names.iteritems(): + src_ip, target_ip = _get_nat_ips(chain_type, fip) + if chain_type == 'pre-routing': + nat_type = 'dnat' + else: + nat_type = 'snat' + self.client.add_static_nat(tenant_id, name, src_ip, + target_ip, + link_port.get_id(), + nat_type, **props) + # disassociate floating IP elif floatingip['floatingip']['port_id'] is None: fip = super(MidonetPluginV2, self).get_floatingip(context, id) - self.client.clear_floating_ip(fip['router_id'], - self.provider_router, - fip['floating_ip_address'], id) + router = self.client.get_router(fip["router_id"]) + self.client.remove_static_route(self._get_provider_router(), + fip["floating_ip_address"]) + + chain_names = _nat_chain_names(router.get_id()) + for _type, name in chain_names.iteritems(): + self.client.remove_rules_by_property( + router.get_tenant_id(), name, OS_FLOATING_IP_RULE_KEY, + id) + super(MidonetPluginV2, self).update_floatingip(context, id, floatingip) LOG.debug(_("MidonetPluginV2.update_floating_ip exiting: fip=%s"), fip) return fip - # - # Security groups supporting methods - # - def create_security_group(self, context, security_group, default_sg=False): - """Create chains for Neutron security group.""" + """Create security group. + + Create a new security group, including the default security group. + In MidoNet, this means creating a pair of chains, inbound and outbound, + as well as a new port group. + """ LOG.debug(_("MidonetPluginV2.create_security_group called: " "security_group=%(security_group)s " "default_sg=%(default_sg)s "), @@ -550,103 +1017,137 @@ class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2, sg = security_group.get('security_group') tenant_id = self._get_tenant_id_for_create(context, sg) + if not default_sg: + self._ensure_default_security_group(context, tenant_id) - with context.session.begin(subtransactions=True): - sg_db_entry = super(MidonetPluginV2, self).create_security_group( - context, security_group, default_sg) + # Create the Neutron sg first + sg = super(MidonetPluginV2, self).create_security_group( + context, security_group, default_sg) - # Create MidoNet chains and portgroup for the SG - self.client.create_for_sg(tenant_id, sg_db_entry['id'], - sg_db_entry['name']) + try: + # Process the MidoNet side + self.client.create_port_group(tenant_id, + _sg_port_group_name(sg["id"])) + chain_names = _sg_chain_names(sg["id"]) + chains = {} + for direction, chain_name in chain_names.iteritems(): + c = self.client.create_chain(tenant_id, chain_name) + chains[direction] = c - LOG.debug(_("MidonetPluginV2.create_security_group exiting: " - "sg_db_entry=%r"), sg_db_entry) - return sg_db_entry + # Create all the rules for this SG. Only accept rules are created + for r in sg['security_group_rules']: + self._create_accept_chain_rule(context, r, + chain=chains[r['direction']]) + except Exception: + LOG.error(_("Failed to create MidoNet resources for sg %(sg)r"), + {"sg": sg}) + with excutils.save_and_reraise_exception(): + with context.session.begin(subtransactions=True): + sg = self._get_security_group(context, sg["id"]) + context.session.delete(sg) + + LOG.debug(_("MidonetPluginV2.create_security_group exiting: sg=%r"), + sg) + return sg def delete_security_group(self, context, id): """Delete chains for Neutron security group.""" LOG.debug(_("MidonetPluginV2.delete_security_group called: id=%s"), id) with context.session.begin(subtransactions=True): - sg_db_entry = super(MidonetPluginV2, self).get_security_group( - context, id) - - if not sg_db_entry: + sg = super(MidonetPluginV2, self).get_security_group(context, id) + if not sg: raise ext_sg.SecurityGroupNotFound(id=id) - sg_name = sg_db_entry['name'] - sg_id = sg_db_entry['id'] - tenant_id = sg_db_entry['tenant_id'] - - if sg_name == 'default' and not context.is_admin: + if sg["name"] == 'default' and not context.is_admin: raise ext_sg.SecurityGroupCannotRemoveDefault() + sg_id = sg['id'] filters = {'security_group_id': [sg_id]} if super(MidonetPluginV2, self)._get_port_security_group_bindings( - context, filters): + context, filters): raise ext_sg.SecurityGroupInUse(id=sg_id) # Delete MidoNet Chains and portgroup for the SG - self.client.delete_for_sg(tenant_id, sg_id, sg_name) + tenant_id = sg['tenant_id'] + self.client.delete_chains_by_names( + tenant_id, _sg_chain_names(sg["id"]).values()) - return super(MidonetPluginV2, self).delete_security_group( - context, id) + self.client.delete_port_group_by_name( + tenant_id, _sg_port_group_name(sg["id"])) - def get_security_groups(self, context, filters=None, fields=None, - default_sg=False): - LOG.debug(_("MidonetPluginV2.get_security_groups called: " - "filters=%(filters)r fields=%(fields)r"), - {'filters': filters, 'fields': fields}) - return super(MidonetPluginV2, self).get_security_groups( - context, filters, fields, default_sg=default_sg) - - def get_security_group(self, context, id, fields=None, tenant_id=None): - LOG.debug(_("MidonetPluginV2.get_security_group called: id=%(id)s " - "fields=%(fields)r tenant_id=%(tenant_id)s"), - {'id': id, 'fields': fields, 'tenant_id': tenant_id}) - return super(MidonetPluginV2, self).get_security_group(context, id, - fields) + super(MidonetPluginV2, self).delete_security_group(context, id) def create_security_group_rule(self, context, security_group_rule): + """Create a security group rule + + Create a security group rule in the Neutron DB and corresponding + MidoNet resources in its data store. + """ LOG.debug(_("MidonetPluginV2.create_security_group_rule called: " "security_group_rule=%(security_group_rule)r"), {'security_group_rule': security_group_rule}) with context.session.begin(subtransactions=True): - rule_db_entry = super( - MidonetPluginV2, self).create_security_group_rule( - context, security_group_rule) + rule = super(MidonetPluginV2, self).create_security_group_rule( + context, security_group_rule) + + self._create_accept_chain_rule(context, rule) - self.client.create_for_sg_rule(rule_db_entry) LOG.debug(_("MidonetPluginV2.create_security_group_rule exiting: " - "rule_db_entry=%r"), rule_db_entry) - return rule_db_entry + "rule=%r"), rule) + return rule - def delete_security_group_rule(self, context, sgrid): + def delete_security_group_rule(self, context, sg_rule_id): + """Delete a security group rule + + Delete a security group rule from the Neutron DB and corresponding + MidoNet resources from its data store. + """ LOG.debug(_("MidonetPluginV2.delete_security_group_rule called: " - "sgrid=%s"), sgrid) - + "sg_rule_id=%s"), sg_rule_id) with context.session.begin(subtransactions=True): - rule_db_entry = super(MidonetPluginV2, - self).get_security_group_rule(context, sgrid) + rule = super(MidonetPluginV2, self).get_security_group_rule( + context, sg_rule_id) - if not rule_db_entry: - raise ext_sg.SecurityGroupRuleNotFound(id=sgrid) + if not rule: + raise ext_sg.SecurityGroupRuleNotFound(id=sg_rule_id) - self.client.delete_for_sg_rule(rule_db_entry) - return super(MidonetPluginV2, - self).delete_security_group_rule(context, sgrid) + sg = self._get_security_group(context, + rule["security_group_id"]) + chain_name = _sg_chain_names(sg["id"])[rule["direction"]] + self.client.remove_rules_by_property(rule["tenant_id"], chain_name, + OS_SG_RULE_KEY, + str(rule["id"])) + super(MidonetPluginV2, self).delete_security_group_rule( + context, sg_rule_id) - def get_security_group_rules(self, context, filters=None, fields=None): - LOG.debug(_("MidonetPluginV2.get_security_group_rules called: " - "filters=%(filters)r fields=%(fields)r"), - {'filters': filters, 'fields': fields}) - return super(MidonetPluginV2, self).get_security_group_rules( - context, filters, fields) + def _add_chain_rule(self, chain, action, **kwargs): - def get_security_group_rule(self, context, id, fields=None): - LOG.debug(_("MidonetPluginV2.get_security_group_rule called: " - "id=%(id)s fields=%(fields)r"), - {'id': id, 'fields': fields}) - return super(MidonetPluginV2, self).get_security_group_rule( - context, id, fields) + nw_proto = kwargs.get("nw_proto") + src_addr = kwargs.pop("src_addr", None) + dst_addr = kwargs.pop("dst_addr", None) + src_port_from = kwargs.pop("src_port_from", None) + src_port_to = kwargs.pop("src_port_to", None) + dst_port_from = kwargs.pop("dst_port_from", None) + dst_port_to = kwargs.pop("dst_port_to", None) + + # Convert to the keys and values that midonet client understands + if src_addr: + kwargs["nw_src_addr"], kwargs["nw_src_length"] = net_util.net_addr( + src_addr) + + if dst_addr: + kwargs["nw_dst_addr"], kwargs["nw_dst_length"] = net_util.net_addr( + dst_addr) + + kwargs["tp_src"] = {"start": src_port_from, "end": src_port_to} + + kwargs["tp_dst"] = {"start": dst_port_from, "end": dst_port_to} + + if nw_proto == 1: # ICMP + # Overwrite port fields regardless of the direction + kwargs["tp_src"] = {"start": src_port_from, "end": src_port_from} + kwargs["tp_dst"] = {"start": dst_port_to, "end": dst_port_to} + + return self.client.add_chain_rule(chain, action=action, **kwargs) diff --git a/neutron/tests/unit/midonet/mock_lib.py b/neutron/tests/unit/midonet/mock_lib.py index 8b9224ee4..6628b50f0 100644 --- a/neutron/tests/unit/midonet/mock_lib.py +++ b/neutron/tests/unit/midonet/mock_lib.py @@ -62,26 +62,6 @@ def get_chain_mock(id=None, tenant_id='test-tenant', name='chain', return chain -def get_exterior_bridge_port_mock(id=None, bridge_id=None): - if id is None: - id = str(uuid.uuid4()) - if bridge_id is None: - bridge_id = str(uuid.uuid4()) - - return get_bridge_port_mock(id=id, bridge_id=bridge_id, - type='ExteriorBridge') - - -def get_interior_bridge_port_mock(id=None, bridge_id=None): - if id is None: - id = str(uuid.uuid4()) - if bridge_id is None: - bridge_id = str(uuid.uuid4()) - - return get_bridge_port_mock(id=id, bridge_id=bridge_id, - type='InteriorBridge') - - def get_port_group_mock(id=None, tenant_id='test-tenant', name='pg'): if id is None: id = str(uuid.uuid4()) @@ -143,22 +123,19 @@ class MidonetLibMockConfig(): def _create_bridge(self, tenant_id, name): return get_bridge_mock(tenant_id=tenant_id, name=name) - def _create_exterior_bridge_port(self, bridge): - return get_exterior_bridge_port_mock(bridge_id=bridge.get_id()) - - def _create_interior_bridge_port(self, bridge): - return get_interior_bridge_port_mock(bridge_id=bridge.get_id()) - def _create_subnet(self, bridge, gateway_ip, subnet_prefix, subnet_len): return get_subnet_mock(bridge.get_id(), gateway_ip=gateway_ip, subnet_prefix=subnet_prefix, subnet_len=subnet_len) + def _add_bridge_port(self, bridge): + return get_bridge_port_mock(bridge_id=bridge.get_id()) + def _get_bridge(self, id): return get_bridge_mock(id=id) def _get_port(self, id): - return get_exterior_bridge_port_mock(id=id) + return get_bridge_port_mock(id=id) def _get_router(self, id): return get_router_mock(id=id) @@ -176,10 +153,8 @@ class MidonetLibMockConfig(): self.inst.create_subnet.side_effect = self._create_subnet # Port methods side effects - ex_bp = self.inst.create_exterior_bridge_port - ex_bp.side_effect = self._create_exterior_bridge_port - in_bp = self.inst.create_interior_bridge_port - in_bp.side_effect = self._create_interior_bridge_port + ex_bp = self.inst.add_bridge_port + ex_bp.side_effect = self._add_bridge_port self.inst.get_port.side_effect = self._get_port # Router methods side effects @@ -206,6 +181,26 @@ class MidoClientMockConfig(): def _get_bridge(self, id): return get_bridge_mock(id=id) + def _get_chain(self, id, query=None): + if not self.chains_in: + return [] + + tenant_id = self._get_query_tenant_id(query) + for chain in self.chains_in: + chain_id = chain['id'] + if chain_id is id: + rule_mocks = [] + if 'rules' in chain: + for rule in chain['rules']: + rule_mocks.append( + get_rule_mock(id=rule['id'], + chain_id=id, + properties=rule['properties'])) + + return get_chain_mock(id=chain_id, name=chain['name'], + tenant_id=tenant_id, rules=rule_mocks) + return None + def _get_chains(self, query=None): if not self.chains_in: return [] @@ -249,5 +244,6 @@ class MidoClientMockConfig(): def setup(self): self.inst.get_bridge.side_effect = self._get_bridge self.inst.get_chains.side_effect = self._get_chains + self.inst.get_chain.side_effect = self._get_chain self.inst.get_port_groups.side_effect = self._get_port_groups self.inst.get_router.side_effect = self._get_router diff --git a/neutron/tests/unit/midonet/test_midonet_driver.py b/neutron/tests/unit/midonet/test_midonet_driver.py new file mode 100644 index 000000000..4bae5176d --- /dev/null +++ b/neutron/tests/unit/midonet/test_midonet_driver.py @@ -0,0 +1,116 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2012 Midokura Japan K.K. +# Copyright (C) 2013 Midokura PTE LTD +# All Rights Reserved. +# +# 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. +# +# @author: Rossella Sblendido, Midokura Japan KK + +import mock +from oslo.config import cfg +import sys +sys.modules["midonetclient"] = mock.Mock() + +from neutron.agent.common import config +from neutron.agent.linux import interface +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils +from neutron.openstack.common import uuidutils +import neutron.plugins.midonet.agent.midonet_driver as driver +from neutron.tests import base + + +class MidoInterfaceDriverTestCase(base.BaseTestCase): + def setUp(self): + self.conf = config.setup_conf() + self.conf.register_opts(interface.OPTS) + config.register_root_helper(self.conf) + self.ip_dev_p = mock.patch.object(ip_lib, 'IPDevice') + self.ip_dev = self.ip_dev_p.start() + self.ip_p = mock.patch.object(ip_lib, 'IPWrapper') + self.ip = self.ip_p.start() + self.device_exists_p = mock.patch.object(ip_lib, 'device_exists') + self.device_exists = self.device_exists_p.start() + + self.api_p = mock.patch.object(sys.modules["midonetclient"].api, + 'MidonetApi') + self.api = self.api_p.start() + self.addCleanup(mock.patch.stopall) + midonet_opts = [ + cfg.StrOpt('midonet_uri', + default='http://localhost:8080/midonet-api', + help=_('MidoNet API server URI.')), + cfg.StrOpt('username', default='admin', + help=_('MidoNet admin username.')), + cfg.StrOpt('password', default='passw0rd', + secret=True, + help=_('MidoNet admin password.')), + cfg.StrOpt('project_id', + default='77777777-7777-7777-7777-777777777777', + help=_('ID of the project that MidoNet admin user' + 'belongs to.')) + ] + self.conf.register_opts(midonet_opts, "MIDONET") + self.driver = driver.MidonetInterfaceDriver(self.conf) + self.network_id = uuidutils.generate_uuid() + self.port_id = uuidutils.generate_uuid() + self.device_name = "tap0" + self.mac_address = "aa:bb:cc:dd:ee:ff" + self.bridge = "br-test" + self.namespace = "ns-test" + super(MidoInterfaceDriverTestCase, self).setUp() + + def test_plug(self): + def device_exists(dev, root_helper=None, namespace=None): + return False + + self.device_exists.side_effect = device_exists + root_dev = mock.Mock() + ns_dev = mock.Mock() + self.ip().add_veth = mock.Mock(return_value=(root_dev, ns_dev)) + self.driver._get_host_uuid = mock.Mock( + return_value=uuidutils.generate_uuid()) + with mock.patch.object(utils, 'execute'): + self.driver.plug( + self.network_id, self.port_id, + self.device_name, self.mac_address, + self.bridge, self.namespace) + + expected = [mock.call(), mock.call('sudo'), + mock.call().add_veth(self.device_name, + self.device_name, + namespace2=self.namespace), + mock.call().ensure_namespace(self.namespace), + mock.call().ensure_namespace().add_device_to_namespace( + mock.ANY)] + ns_dev.assert_has_calls( + [mock.call.link.set_address(self.mac_address)]) + + root_dev.assert_has_calls([mock.call.link.set_up()]) + ns_dev.assert_has_calls([mock.call.link.set_up()]) + self.ip.assert_has_calls(expected, True) + host = mock.Mock() + self.api().get_host = mock.Mock(return_value=host) + self.api.assert_has_calls([mock.call().add_host_interface_port]) + + def test_unplug(self): + with mock.patch.object(utils, 'execute'): + self.driver.unplug(self.device_name, self.bridge, self.namespace) + + self.ip_dev.assert_has_calls([ + mock.call(self.device_name, self.driver.root_helper, + self.namespace), + mock.call().link.delete()]) + self.ip.assert_has_calls(mock.call().garbage_collect_namespace()) diff --git a/neutron/tests/unit/midonet/test_midonet_lib.py b/neutron/tests/unit/midonet/test_midonet_lib.py index db1b461f2..1dd6b3a0e 100644 --- a/neutron/tests/unit/midonet/test_midonet_lib.py +++ b/neutron/tests/unit/midonet/test_midonet_lib.py @@ -19,7 +19,6 @@ # @author: Ryu Ishimoto, Midokura Japan KK # @author: Tomoe Sugihara, Midokura Japan KK - import mock import testtools import webob.exc as w_exc @@ -33,55 +32,8 @@ def _create_test_chain(id, name, tenant_id): return {'id': id, 'name': name, 'tenant_id': tenant_id} -def _create_test_port_group(sg_id, sg_name, id, tenant_id): - return {"id": id, "name": "OS_SG_%s_%s" % (sg_id, sg_name), - "tenant_id": tenant_id} - - -def _create_test_router_in_chain(router_id, id, tenant_id): - name = "OS_ROUTER_IN_%s" % router_id - return _create_test_chain(id, name, tenant_id) - - -def _create_test_router_out_chain(router_id, id, tenant_id): - name = "OS_ROUTER_OUT_%s" % router_id - return _create_test_chain(id, name, tenant_id) - - -def _create_test_rule(id, chain_id, properties): - return {"id": id, "chain_id": chain_id, "properties": properties} - - -def _create_test_sg_in_chain(sg_id, sg_name, id, tenant_id): - if sg_name: - name = "OS_SG_%s_%s_IN" % (sg_id, sg_name) - else: - name = "OS_SG_%s_IN" % sg_id - return _create_test_chain(id, name, tenant_id) - - -def _create_test_sg_out_chain(sg_id, sg_name, id, tenant_id): - if sg_name: - name = "OS_SG_%s_%s_OUT" % (sg_id, sg_name) - else: - name = "OS_SG_%s_OUT" % sg_id - return _create_test_chain(id, name, tenant_id) - - -def _create_test_sg_rule(tenant_id, sg_id, id, - direction="egress", protocol="tcp", port_min=1, - port_max=65535, src_ip='192.168.1.0/24', - src_group_id=None, ethertype=0x0800, properties=None): - return {"tenant_id": tenant_id, "security_group_id": sg_id, - "id": id, "direction": direction, "protocol": protocol, - "remote_ip_prefix": src_ip, "remote_group_id": src_group_id, - "port_range_min": port_min, "port_range_max": port_max, - "ethertype": ethertype, "external_id": None} - - -def _create_test_sg_chain_rule(id, chain_id, sg_rule_id): - props = {"os_sg_rule_id": sg_rule_id} - return _create_test_rule(id, chain_id, props) +def _create_test_port_group(id, name, tenant_id): + return {"id": id, "name": name, "tenant_id": tenant_id} class MidoClientTestCase(testtools.TestCase): @@ -94,137 +46,59 @@ class MidoClientTestCase(testtools.TestCase): self.mock_api_cfg.setup() self.client = midonet_lib.MidoClient(self.mock_api) - def test_create_for_sg(self): - sg_id = uuidutils.generate_uuid() - sg_name = 'test-sg' - calls = [mock.call.add_chain().tenant_id(self._tenant_id), - mock.call.add_port_group().tenant_id(self._tenant_id)] + def test_delete_chains_by_names(self): - self.client.create_for_sg(self._tenant_id, sg_id, sg_name) + tenant_id = uuidutils.generate_uuid() + chain1_id = uuidutils.generate_uuid() + chain1 = _create_test_chain(chain1_id, "chain1", tenant_id) + + chain2_id = uuidutils.generate_uuid() + chain2 = _create_test_chain(chain2_id, "chain2", tenant_id) + + calls = [mock.call.delete_chain(chain1_id), + mock.call.delete_chain(chain2_id)] + self.mock_api_cfg.chains_in = [chain2, chain1] + self.client.delete_chains_by_names(tenant_id, ["chain1", "chain2"]) self.mock_api.assert_has_calls(calls, any_order=True) - def test_create_for_sg_rule(self): - sg_id = uuidutils.generate_uuid() - sg_name = 'test-sg' - in_chain_id = uuidutils.generate_uuid() - out_chain_id = uuidutils.generate_uuid() - self.mock_api_cfg.chains_in = [ - _create_test_sg_in_chain(sg_id, sg_name, in_chain_id, - self._tenant_id), - _create_test_sg_out_chain(sg_id, sg_name, out_chain_id, - self._tenant_id)] + def test_delete_port_group_by_name(self): - sg_rule_id = uuidutils.generate_uuid() - sg_rule = _create_test_sg_rule(self._tenant_id, sg_id, sg_rule_id) + tenant_id = uuidutils.generate_uuid() + pg1_id = uuidutils.generate_uuid() + pg1 = _create_test_port_group(pg1_id, "pg1", tenant_id) + pg2_id = uuidutils.generate_uuid() + pg2 = _create_test_port_group(pg2_id, "pg2", tenant_id) - props = {"os_sg_rule_id": sg_rule_id} - calls = [mock.call.add_rule().port_group(None).type( - 'accept').nw_proto(6).nw_src_address( - '192.168.1.0').nw_src_length(24).tp_src_start( - None).tp_src_end(None).tp_dst_start(1).tp_dst_end( - 65535).properties(props).create()] + self.mock_api_cfg.port_groups_in = [pg1, pg2] + self.client.delete_port_group_by_name(tenant_id, "pg1") + self.mock_api.delete_port_group.assert_called_once_with(pg1_id) - self.client.create_for_sg_rule(sg_rule) + def test_create_dhcp(self): - # Egress chain rule added - self.mock_api_cfg.chains_out[0].assert_has_calls(calls) + bridge = mock.Mock() + gw_call = mock.call.add_dhcp_subnet().default_gateway("192.168.1.1") + subnet_prefix_call = gw_call.subnet_prefix("192.168.1.0") + subnet_length_call = subnet_prefix_call.subnet_length(24) - def test_create_router_chains(self): - router = mock_lib.get_router_mock(tenant_id=self._tenant_id) - api_calls = [mock.call.add_chain().tenant_id(self._tenant_id)] - router_calls = [ - mock.call.inbound_filter_id().outbound_filter_id().update()] + calls = [gw_call, subnet_prefix_call, subnet_length_call, + subnet_length_call.create()] - self.client.create_router_chains(router) + self.client.create_dhcp(bridge, "192.168.1.1", "192.168.1.0/24") + bridge.assert_has_calls(calls, any_order=True) - self.mock_api.assert_has_calls(api_calls) - router.assert_has_calls(router_calls) + def test_add_dhcp_host(self): - def test_delete_for_sg(self): - sg_id = uuidutils.generate_uuid() - sg_name = 'test-sg' - in_chain_id = uuidutils.generate_uuid() - out_chain_id = uuidutils.generate_uuid() - pg_id = uuidutils.generate_uuid() - self.mock_api_cfg.chains_in = [ - _create_test_sg_in_chain(sg_id, sg_name, in_chain_id, - self._tenant_id), - _create_test_sg_out_chain(sg_id, sg_name, out_chain_id, - self._tenant_id)] - self.mock_api_cfg.port_groups_in = [ - _create_test_port_group(sg_id, sg_name, pg_id, self._tenant_id)] + bridge = mock.Mock() + dhcp_subnet_call = mock.call.get_dhcp_subnet("10.0.0.0_24") + ip_addr_call = dhcp_subnet_call.add_dhcp_host().ip_addr("10.0.0.10") + mac_addr_call = ip_addr_call.mac_addr("2A:DB:6B:8C:19:99") + calls = [dhcp_subnet_call, ip_addr_call, mac_addr_call, + mac_addr_call.create()] - calls = [mock.call.get_chains({"tenant_id": self._tenant_id}), - mock.call.delete_chain(in_chain_id), - mock.call.delete_chain(out_chain_id), - mock.call.get_port_groups({"tenant_id": self._tenant_id}), - mock.call.delete_port_group(pg_id)] - - self.client.delete_for_sg(self._tenant_id, sg_id, sg_name) - - self.mock_api.assert_has_calls(calls) - - def test_delete_for_sg_rule(self): - sg_id = uuidutils.generate_uuid() - sg_name = 'test-sg' - in_chain_id = uuidutils.generate_uuid() - out_chain_id = uuidutils.generate_uuid() - self.mock_api_cfg.chains_in = [ - _create_test_sg_in_chain(sg_id, sg_name, in_chain_id, - self._tenant_id), - _create_test_sg_out_chain(sg_id, sg_name, out_chain_id, - self._tenant_id)] - - rule_id = uuidutils.generate_uuid() - sg_rule_id = uuidutils.generate_uuid() - rule = _create_test_sg_chain_rule(rule_id, in_chain_id, sg_rule_id) - self.mock_api_cfg.chains_in[0]['rules'] = [rule] - sg_rule = _create_test_sg_rule(self._tenant_id, sg_id, sg_rule_id) - - self.client.delete_for_sg_rule(sg_rule) - - self.mock_api.delete_rule.assert_called_once_with(rule_id) - - def test_get_bridge(self): - bridge_id = uuidutils.generate_uuid() - - bridge = self.client.get_bridge(bridge_id) - - self.assertIsNotNone(bridge) - self.assertEqual(bridge.get_id(), bridge_id) - - def test_get_bridge_error(self): - self.mock_api.get_bridge.side_effect = w_exc.HTTPInternalServerError() - self.assertRaises(midonet_lib.MidonetApiException, - self.client.get_bridge, uuidutils.generate_uuid()) - - def test_get_bridge_not_found(self): - self.mock_api.get_bridge.side_effect = w_exc.HTTPNotFound() - self.assertRaises(midonet_lib.MidonetResourceNotFound, - self.client.get_bridge, uuidutils.generate_uuid()) - - def test_get_port_groups_for_sg(self): - sg_id = uuidutils.generate_uuid() - pg_id = uuidutils.generate_uuid() - self.mock_api_cfg.port_groups_in = [ - _create_test_port_group(sg_id, 'test-sg', pg_id, self._tenant_id)] - - pg = self.client.get_port_groups_for_sg(self._tenant_id, sg_id) - - self.assertIsNotNone(pg) - self.assertEqual(pg.get_id(), pg_id) - - def _create_test_rule(self, tenant_id, sg_id, rule_id, direction="egress", - protocol="tcp", port_min=1, port_max=65535, - src_ip='192.168.1.0/24', src_group_id=None, - ethertype=0x0800): - return {"tenant_id": tenant_id, "security_group_id": sg_id, - "rule_id": rule_id, "direction": direction, - "protocol": protocol, - "remote_ip_prefix": src_ip, "remote_group_id": src_group_id, - "port_range_min": port_min, "port_range_max": port_max, - "ethertype": ethertype, "id": rule_id, "external_id": None} + self.client.add_dhcp_host(bridge, "10.0.0.0/24", "10.0.0.10", + "2A:DB:6B:8C:19:99") + bridge.assert_has_calls(calls, any_order=True) def test_get_router_error(self): self.mock_api.get_router.side_effect = w_exc.HTTPInternalServerError() @@ -236,43 +110,20 @@ class MidoClientTestCase(testtools.TestCase): self.assertRaises(midonet_lib.MidonetResourceNotFound, self.client.get_router, uuidutils.generate_uuid()) - def test_get_router_chains(self): - router_id = uuidutils.generate_uuid() - in_chain_id = uuidutils.generate_uuid() - out_chain_id = uuidutils.generate_uuid() - self.mock_api_cfg.chains_in = [ - _create_test_router_in_chain(router_id, in_chain_id, - self._tenant_id), - _create_test_router_out_chain(router_id, out_chain_id, - self._tenant_id)] + def test_get_bridge_error(self): + self.mock_api.get_bridge.side_effect = w_exc.HTTPInternalServerError() + self.assertRaises(midonet_lib.MidonetApiException, + self.client.get_bridge, uuidutils.generate_uuid()) - chains = self.client.get_router_chains(self._tenant_id, router_id) + def test_get_bridge_not_found(self): + self.mock_api.get_bridge.side_effect = w_exc.HTTPNotFound() + self.assertRaises(midonet_lib.MidonetResourceNotFound, + self.client.get_bridge, uuidutils.generate_uuid()) - self.mock_api.assert_has_calls(mock.call.get_chains( - {"tenant_id": self._tenant_id})) - self.assertEqual(len(chains), 2) - self.assertIn('in', chains) - self.assertIn('out', chains) - self.assertEqual(chains['in'].get_id(), in_chain_id) - self.assertEqual(chains['out'].get_id(), out_chain_id) + def test_get_bridge(self): + bridge_id = uuidutils.generate_uuid() - def test_get_sg_chains(self): - sg_id = uuidutils.generate_uuid() - sg_name = 'test-sg' - in_chain_id = uuidutils.generate_uuid() - out_chain_id = uuidutils.generate_uuid() - self.mock_api_cfg.chains_in = [ - _create_test_sg_in_chain(sg_id, sg_name, in_chain_id, - self._tenant_id), - _create_test_sg_out_chain(sg_id, sg_name, out_chain_id, - self._tenant_id)] + bridge = self.client.get_bridge(bridge_id) - chains = self.client.get_sg_chains(self._tenant_id, sg_id) - - self.mock_api.assert_has_calls(mock.call.get_chains( - {"tenant_id": self._tenant_id})) - self.assertEqual(len(chains), 2) - self.assertIn('in', chains) - self.assertIn('out', chains) - self.assertEqual(chains['in'].get_id(), in_chain_id) - self.assertEqual(chains['out'].get_id(), out_chain_id) + self.assertIsNotNone(bridge) + self.assertEqual(bridge.get_id(), bridge_id)