Ryu Ishimoto 23aec9a5bb Make MidoNet plugin code more testable
Refactor plugin.py and midonet_lib.py to improve unit testing of
the MidoNet plugin.  Make all the tests pass in the plugin except
those that test features that are not currently supported by the
plugin, which are IPv6 and multiple subnets per network.
Consolidate the mocking code in mock_lib.py to be shared by
plugin.py and midonet_lib.py.

Change-Id: Idcdfc3d90d5ec9e0be29bb6833d336af596fe363
Fixes: bug #1133215
Signed-off-by: Ryu Ishimoto <ryu@midokura.com>
Signed-off-by: Tomoe Sugihara <tomoe@midokura.com>
2013-05-28 02:26:09 +09:00

675 lines
29 KiB
Python

# 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: Takaaki Suzuki, 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 quantum.common import exceptions as q_exc
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import l3_db
from quantum.db import models_v2
from quantum.db import securitygroups_db
from quantum.extensions import securitygroup as ext_sg
from quantum.openstack.common import log as logging
from quantum.plugins.midonet import config # noqa
from quantum.plugins.midonet import midonet_lib
LOG = logging.getLogger(__name__)
class MidonetPluginException(q_exc.QuantumException):
message = _("%(msg)s")
class MidonetPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin,
securitygroups_db.SecurityGroupDbMixin):
supported_extension_aliases = ['router', 'security-group']
__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.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)
db.configure_db()
def create_subnet(self, context, subnet):
"""Create Quantum subnet.
Creates a Quantum subnet and a DHCP entry in MidoNet bridge.
"""
LOG.debug(_("MidonetPluginV2.create_subnet called: subnet=%r"), subnet)
if subnet['subnet']['ip_version'] == 6:
raise q_exc.NotImplementedError(
_("MidoNet doesn't support IPv6."))
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):
sn_entry = super(MidonetPluginV2, self).create_subnet(context,
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)
# For external network, link the bridge to the provider router.
self._extend_network_dict_l3(context, net)
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)
LOG.debug(_("MidonetPluginV2.create_subnet exiting: sn_entry=%r"),
sn_entry)
return sn_entry
def delete_subnet(self, context, id):
"""Delete Quantum subnet.
Delete quantum network and its corresponding MidoNet bridge.
"""
LOG.debug(_("MidonetPluginV2.delete_subnet called: id=%s"), id)
subnet = super(MidonetPluginV2, self).get_subnet(context, id,
fields=None)
net = super(MidonetPluginV2, self).get_network(context,
subnet['network_id'],
fields=None)
bridge = self.client.get_bridge(subnet['network_id'])
self.client.delete_dhcp(bridge)
# If the network is external, clean up routes, links, ports.
self._extend_network_dict_l3(context, net)
if net['router:external']:
self.client.unlink_bridge_from_provider_router(
bridge, self.provider_router)
super(MidonetPluginV2, self).delete_subnet(context, id)
LOG.debug(_("MidonetPluginV2.delete_subnet exiting"))
def create_network(self, context, network):
"""Create Quantum network.
Create a new Quantum network and its corresponding MidoNet bridge.
"""
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)
session = context.session
with session.begin(subtransactions=True):
bridge = self.client.create_bridge(tenant_id,
network['network']['name'])
# Set MidoNet bridge ID to the quantum 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, network['network'], net['id'])
self._extend_network_dict_l3(context, net)
LOG.debug(_("MidonetPluginV2.create_network exiting: net=%r"), net)
return net
def update_network(self, context, id, network):
"""Update Quantum network.
Update an existing Quantum network and its corresponding MidoNet
bridge.
"""
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(
context, id, network)
self.client.update_bridge(id, net['name'])
self._extend_network_dict_l3(context, net)
LOG.debug(_("MidonetPluginV2.update_network exiting: net=%r"), net)
return net
def get_network(self, context, id, fields=None):
"""Get Quantum network.
Retrieves a Quantum network and its corresponding MidoNet bridge.
"""
LOG.debug(_("MidonetPluginV2.get_network called: id=%(id)r, "
"fields=%(fields)r"), {'id': id, 'fields': fields})
# NOTE: Get network data with all fields (fields=None) for
# _extend_network_dict_l3() method, which needs 'id' field
qnet = super(MidonetPluginV2, self).get_network(context, id, None)
self.client.get_bridge(id)
self._extend_network_dict_l3(context, qnet)
LOG.debug(_("MidonetPluginV2.get_network exiting: qnet=%r"), qnet)
return self._fields(qnet, fields)
def get_networks(self, context, filters=None, fields=None):
"""List quantum networks and verify that all exist in MidoNet."""
LOG.debug(_("MidonetPluginV2.get_networks called: "
"filters=%(filters)r, fields=%(fields)r"),
{'filters': filters, 'fields': fields})
# NOTE: Get network data with all fields (fields=None) for
# _extend_network_dict_l3() method, which needs 'id' field
qnets = super(MidonetPluginV2, self).get_networks(context, filters,
None)
for n in qnets:
self._extend_network_dict_l3(context, n)
return [self._fields(net, fields) for net in qnets]
def delete_network(self, context, id):
"""Delete a network and its corresponding MidoNet bridge."""
LOG.debug(_("MidonetPluginV2.delete_network called: id=%r"), id)
self.client.delete_bridge(id)
try:
super(MidonetPluginV2, self).delete_network(context, id)
except Exception:
LOG.error(_('Failed to delete quantum db, while Midonet bridge=%r'
'had been deleted'), id)
raise
def create_port(self, context, port):
"""Create a L2 port in Quantum/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']
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):
# 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)
if bridge_port:
# set midonet port id to quantum port id and create a DB record.
port_data['id'] = bridge_port.get_id()
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
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 quantum 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
def get_ports(self, context, filters=None, fields=None):
"""List quantum 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
def delete_port(self, context, id, l3_port_check=True):
"""Delete a quantum port and corresponding MidoNet bridge port."""
LOG.debug(_("MidonetPluginV2.delete_port called: id=%(id)s "
"l3_port_check=%(l3_port_check)r"),
{'id': id, 'l3_port_check': l3_port_check})
# if needed, check to see if this is a port owned by
# and l3-router. If so, we should prevent deletion.
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']
# create dhcp host entry under the bridge.
self.client.delete_dhcp_hosts(port_db_entry['network_id'], ip,
mac)
self.client.delete_port(id)
return super(MidonetPluginV2, self).delete_port(context, id)
#
# L3 APIs.
#
def create_router(self, context, router):
LOG.debug(_("MidonetPluginV2.create_router called: router=%r"), router)
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
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)
qrouter = super(MidonetPluginV2, self).create_router(context,
router)
# 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)
LOG.debug(_("MidonetPluginV2.create_router exiting: qrouter=%r"),
qrouter)
return qrouter
def update_router(self, context, id, router):
LOG.debug(_("MidonetPluginV2.update_router called: id=%(id)s "
"router=%(router)r"), router)
if router['router'].get('admin_state_up') is False:
raise q_exc.NotImplementedError(_('admin_state_up=False '
'routers are not '
'supported.'))
op_gateway_set = False
op_gateway_clear = False
# 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
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']
qport['network_id']
session = context.session
with session.begin(subtransactions=True):
qrouter = super(MidonetPluginV2, self).update_router(context, id,
router)
changed_name = router['router'].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
def delete_router(self, context, id):
LOG.debug(_("MidonetPluginV2.delete_router called: id=%s"), id)
self.client.delete_tenant_router(id)
result = super(MidonetPluginV2, self).delete_router(context, id)
LOG.debug(_("MidonetPluginV2.delete_router exiting: result=%s"),
result)
return result
def add_router_interface(self, context, router_id, interface_info):
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)
# 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)
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)
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 quantum 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"))
return info
def update_floatingip(self, context, id, floatingip):
LOG.debug(_("MidonetPluginV2.update_floatingip called: id=%(id)s "
"floatingip=%(floatingip)s "),
{'id': id, 'floatingip': floatingip})
session = context.session
with session.begin(subtransactions=True):
if floatingip['floatingip']['port_id']:
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)
# 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)
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 Quantum security group."""
LOG.debug(_("MidonetPluginV2.create_security_group called: "
"security_group=%(security_group)s "
"default_sg=%(default_sg)s "),
{'security_group': security_group, 'default_sg': default_sg})
sg = security_group.get('security_group')
tenant_id = self._get_tenant_id_for_create(context, sg)
with context.session.begin(subtransactions=True):
sg_db_entry = 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'])
LOG.debug(_("MidonetPluginV2.create_security_group exiting: "
"sg_db_entry=%r"), sg_db_entry)
return sg_db_entry
def delete_security_group(self, context, id):
"""Delete chains for Quantum 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:
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:
raise ext_sg.SecurityGroupCannotRemoveDefault()
filters = {'security_group_id': [sg_id]}
if super(MidonetPluginV2, self)._get_port_security_group_bindings(
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)
return super(MidonetPluginV2, self).delete_security_group(
context, 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)
def create_security_group_rule(self, context, security_group_rule):
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)
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
def delete_security_group_rule(self, context, sgrid):
LOG.debug(_("MidonetPluginV2.delete_security_group_rule called: "
"sgrid=%s"), sgrid)
with context.session.begin(subtransactions=True):
rule_db_entry = super(MidonetPluginV2,
self).get_security_group_rule(context, sgrid)
if not rule_db_entry:
raise ext_sg.SecurityGroupRuleNotFound(id=sgrid)
self.client.delete_for_sg_rule(rule_db_entry)
return super(MidonetPluginV2,
self).delete_security_group_rule(context, sgrid)
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 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)