From dbacb52f420364cb85500a9c132207670afef4b5 Mon Sep 17 00:00:00 2001 From: Kevin Benton Date: Mon, 27 Jan 2014 17:26:12 -0800 Subject: [PATCH] Enables BigSwitch/Restproxy ML2 VLAN driver Refactors Bigswitch/Restproxy plugin by separating into reusable libraries that can be used by the plugin as well as the ml2 driver to proxy calls to the backend controller. Enables basic unit tests for the ML2 driver. Removes deprecated separate unplug/plug operations on ports. Implements: blueprint bigswitch-ml2-driver Change-Id: I4e22ba7e20ec4f405b9fd34a1bf08a48544f317d --- .../plugins/bigswitch/db/porttracker_db.py | 7 +- neutron/plugins/bigswitch/plugin.py | 622 ++++++++---------- .../ml2/drivers/mech_bigswitch/__init__.py | 0 .../ml2/drivers/mech_bigswitch/driver.py | 104 +++ .../unit/bigswitch/test_restproxy_plugin.py | 46 +- .../unit/ml2/drivers/test_bigswitch_mech.py | 95 +++ setup.cfg | 2 + 7 files changed, 502 insertions(+), 374 deletions(-) create mode 100644 neutron/plugins/ml2/drivers/mech_bigswitch/__init__.py create mode 100644 neutron/plugins/ml2/drivers/mech_bigswitch/driver.py create mode 100644 neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py diff --git a/neutron/plugins/bigswitch/db/porttracker_db.py b/neutron/plugins/bigswitch/db/porttracker_db.py index 36e8af72d..da5f0c22f 100644 --- a/neutron/plugins/bigswitch/db/porttracker_db.py +++ b/neutron/plugins/bigswitch/db/porttracker_db.py @@ -36,8 +36,13 @@ def put_port_hostid(context, port_id, host): LOG.warning(_("No host_id in port request to track port location.")) return if port_id == '': - LOG.warning(_("Received an empty port ID for host '%s'"), host) + LOG.warning(_("Received an empty port ID for host_id '%s'"), host) return + if host == '': + LOG.debug(_("Received an empty host_id for port '%s'"), port_id) + return + LOG.debug(_("Logging port %(port)s on host_id %(host)s"), + {'port': port_id, 'host': host}) with context.session.begin(subtransactions=True): location = portbindings_db.PortBindingPort(port_id=port_id, host=host) context.session.merge(location) diff --git a/neutron/plugins/bigswitch/plugin.py b/neutron/plugins/bigswitch/plugin.py index f9499d59c..4c1366e61 100644 --- a/neutron/plugins/bigswitch/plugin.py +++ b/neutron/plugins/bigswitch/plugin.py @@ -91,7 +91,7 @@ restproxy_opts = [ "which performs the networking configuration. Note that " "only one server is needed per deployment, but you may " "wish to deploy multiple servers to support failover.")), - cfg.StrOpt('server_auth', default='username:password', secret=True, + cfg.StrOpt('server_auth', default=None, secret=True, help=_("The username and password for authenticating against " " the BigSwitch or Floodlight controller.")), cfg.BoolOpt('server_ssl', default=False, @@ -202,12 +202,12 @@ class ServerProxy(object): headers['Authorization'] = self.auth LOG.debug(_("ServerProxy: server=%(server)s, port=%(port)d, " - "ssl=%(ssl)r, action=%(action)s"), - {'server': self.server, 'port': self.port, 'ssl': self.ssl, + "ssl=%(ssl)r"), + {'server': self.server, 'port': self.port, 'ssl': self.ssl}) + LOG.debug(_("ServerProxy: resource=%(resource)s, action=%(action)s, " + "data=%(data)r, headers=%(headers)r"), + {'resource': resource, 'data': data, 'headers': headers, 'action': action}) - LOG.debug(_("ServerProxy: resource=%(resource)s, data=%(data)r, " - "headers=%(headers)r"), - {'resource': resource, 'data': data, 'headers': headers}) conn = None if self.ssl: @@ -252,17 +252,33 @@ class ServerProxy(object): class ServerPool(object): - def __init__(self, servers, ssl, auth, neutron_id, timeout=10, - base_uri='/quantum/v1.0', name='NeutronRestProxy'): + def __init__(self, timeout=10, + base_uri=BASE_URI, name='NeutronRestProxy'): + LOG.debug(_("ServerPool: initializing")) + # 'servers' is the list of network controller REST end-points + # (used in order specified till one succeeds, and it is sticky + # till next failure). Use 'server_auth' to encode api-key + servers = cfg.CONF.RESTPROXY.servers + self.auth = cfg.CONF.RESTPROXY.server_auth + self.ssl = cfg.CONF.RESTPROXY.server_ssl + self.neutron_id = cfg.CONF.RESTPROXY.neutron_id self.base_uri = base_uri - self.timeout = timeout self.name = name - self.auth = auth - self.ssl = ssl - self.neutron_id = neutron_id - self.servers = [] - for server_port in servers: - self.servers.append(self.server_proxy_for(*server_port)) + timeout = cfg.CONF.RESTPROXY.server_timeout + if timeout is not None: + self.timeout = timeout + + # validate config + if not servers: + raise cfg.Error(_('Servers not defined. Aborting plugin')) + if any((len(spl) != 2) for spl in [sp.split(':', 1) + for sp in servers.split(',')]): + raise cfg.Error(_('Servers must be defined as :')) + self.servers = [ + self.server_proxy_for(server, int(port)) + for server, port in (s.rsplit(':', 1) for s in servers.split(',')) + ] + LOG.debug(_("ServerPool: initialization done")) def server_proxy_for(self, server, port): return ServerProxy(server, port, self.ssl, self.auth, self.neutron_id, @@ -379,39 +395,28 @@ class ServerPool(object): errstr = _("Unable to update remote network: %s") self.rest_action('DELETE', resource, errstr=errstr) - def rest_create_port(self, net, port): - resource = PORT_RESOURCE_PATH % (net["tenant_id"], net["id"]) + def rest_create_port(self, tenant_id, net_id, port): + resource = ATTACHMENT_PATH % (tenant_id, net_id, port["id"]) data = {"port": port} + device_id = port.get("device_id") + if not port["mac_address"] or not device_id: + # controller only cares about ports attached to devices + LOG.warning(_("No device attached to port %s. " + "Skipping notification to controller."), port["id"]) + return + data["attachment"] = {"id": device_id, + "mac": port["mac_address"]} errstr = _("Unable to create remote port: %s") - self.rest_action('POST', resource, data, errstr) - - def rest_update_port(self, tenant_id, network_id, port, port_id): - resource = PORTS_PATH % (tenant_id, network_id, port_id) - data = {"port": port} - errstr = _("Unable to update remote port: %s") self.rest_action('PUT', resource, data, errstr) def rest_delete_port(self, tenant_id, network_id, port_id): - resource = PORTS_PATH % (tenant_id, network_id, port_id) + resource = ATTACHMENT_PATH % (tenant_id, network_id, port_id) errstr = _("Unable to delete remote port: %s") self.rest_action('DELETE', resource, errstr=errstr) - def rest_plug_interface(self, tenant_id, net_id, port, - remote_interface_id): - if port["mac_address"] is not None: - resource = ATTACHMENT_PATH % (tenant_id, net_id, port["id"]) - data = {"attachment": - {"id": remote_interface_id, - "mac": port["mac_address"], - } - } - errstr = _("Unable to plug in interface: %s") - self.rest_action('PUT', resource, data, errstr) - - def rest_unplug_interface(self, tenant_id, net_id, port_id): - resource = ATTACHMENT_PATH % (tenant_id, net_id, port_id) - errstr = _("Unable to unplug interface: %s") - self.rest_action('DELETE', resource, errstr=errstr) + def rest_update_port(self, tenant_id, net_id, port): + # Controller has no update operation for the port endpoint + self.rest_create_port(tenant_id, net_id, port) class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): @@ -423,9 +428,227 @@ class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): agents_db.AgentExtRpcCallback()]) -class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, - external_net_db.External_net_db_mixin, - routerrule_db.RouterRule_db_mixin, +class NeutronRestProxyV2Base(db_base_plugin_v2.NeutronDbPluginV2, + external_net_db.External_net_db_mixin, + routerrule_db.RouterRule_db_mixin): + + supported_extension_aliases = ["binding"] + servers = None + + def __init__(self, server_timeout=None): + # This base class is not intended to be instantiated directly. + # Extending class should set ServerPool. + if not self.servers: + LOG.warning(_("ServerPool not set!")) + + def _send_all_data(self, send_ports=True, send_floating_ips=True, + send_routers=True): + """Pushes all data to network ctrl (networks/ports, ports/attachments). + + This gives the controller an option to re-sync it's persistent store + with neutron's current view of that data. + """ + admin_context = qcontext.get_admin_context() + networks = [] + + all_networks = self.get_networks(admin_context) or [] + for net in all_networks: + mapped_network = self._get_mapped_network_with_subnets(net) + flips_n_ports = {} + if send_floating_ips: + flips_n_ports = self._get_network_with_floatingips( + mapped_network) + + if send_ports: + ports = [] + net_filter = {'network_id': [net.get('id')]} + net_ports = self.get_ports(admin_context, + filters=net_filter) or [] + for port in net_ports: + mapped_port = self._map_state_and_status(port) + mapped_port['attachment'] = { + 'id': port.get('device_id'), + 'mac': port.get('mac_address'), + } + mapped_port = self._extend_port_dict_binding(admin_context, + mapped_port) + ports.append(mapped_port) + flips_n_ports['ports'] = ports + + if flips_n_ports: + networks.append(flips_n_ports) + + resource = '/topology' + data = { + 'networks': networks, + } + + if send_routers: + routers = [] + all_routers = self.get_routers(admin_context) or [] + for router in all_routers: + interfaces = [] + mapped_router = self._map_state_and_status(router) + router_filter = { + 'device_owner': ["network:router_interface"], + 'device_id': [router.get('id')] + } + router_ports = self.get_ports(admin_context, + filters=router_filter) or [] + for port in router_ports: + net_id = port.get('network_id') + subnet_id = port['fixed_ips'][0]['subnet_id'] + intf_details = self._get_router_intf_details(admin_context, + net_id, + subnet_id) + interfaces.append(intf_details) + mapped_router['interfaces'] = interfaces + + routers.append(mapped_router) + + data.update({'routers': routers}) + + errstr = _("Unable to update remote topology: %s") + return self.servers.rest_action('PUT', resource, data, errstr) + + def _get_network_with_floatingips(self, network, context=None): + if context is None: + context = qcontext.get_admin_context() + + net_id = network['id'] + net_filter = {'floating_network_id': [net_id]} + fl_ips = self.get_floatingips(context, + filters=net_filter) or [] + network['floatingips'] = fl_ips + + return network + + def _get_all_subnets_json_for_network(self, net_id, context=None): + if context is None: + context = qcontext.get_admin_context() + # start a sub-transaction to avoid breaking parent transactions + with context.session.begin(subtransactions=True): + subnets = self._get_subnets_by_network(context, + net_id) + subnets_details = [] + if subnets: + for subnet in subnets: + subnet_dict = self._make_subnet_dict(subnet) + mapped_subnet = self._map_state_and_status(subnet_dict) + subnets_details.append(mapped_subnet) + + return subnets_details + + def _get_mapped_network_with_subnets(self, network, context=None): + # if context is not provided, admin context is used + if context is None: + context = qcontext.get_admin_context() + network = self._map_state_and_status(network) + subnets = self._get_all_subnets_json_for_network(network['id'], + context) + network['subnets'] = subnets + for subnet in (subnets or []): + if subnet['gateway_ip']: + # FIX: For backward compatibility with wire protocol + network['gateway'] = subnet['gateway_ip'] + break + else: + network['gateway'] = '' + network[external_net.EXTERNAL] = self._network_is_external( + context, network['id']) + # include ML2 segmentation types + network['segmentation_types'] = getattr(self, "segmentation_types", "") + return network + + def _send_create_network(self, network, context=None): + tenant_id = network['tenant_id'] + mapped_network = self._get_mapped_network_with_subnets(network, + context) + self.servers.rest_create_network(tenant_id, mapped_network) + + def _send_update_network(self, network, context=None): + net_id = network['id'] + tenant_id = network['tenant_id'] + mapped_network = self._get_mapped_network_with_subnets(network, + context) + net_fl_ips = self._get_network_with_floatingips(mapped_network, + context) + self.servers.rest_update_network(tenant_id, net_id, net_fl_ips) + + def _send_delete_network(self, network, context=None): + net_id = network['id'] + tenant_id = network['tenant_id'] + self.servers.rest_delete_network(tenant_id, net_id) + + def _map_state_and_status(self, resource): + resource = copy.copy(resource) + + resource['state'] = ('UP' if resource.pop('admin_state_up', + True) else 'DOWN') + + if 'status' in resource: + del resource['status'] + + return resource + + def _warn_on_state_status(self, resource): + if resource.get('admin_state_up', True) is False: + LOG.warning(_("Setting admin_state_up=False is not supported " + "in this plugin version. Ignoring setting for " + "resource: %s"), resource) + + if 'status' in resource: + if resource['status'] is not const.NET_STATUS_ACTIVE: + LOG.warning(_("Operational status is internally set by the " + "plugin. Ignoring setting status=%s."), + resource['status']) + + def _get_router_intf_details(self, context, intf_id, subnet_id): + + # we will use the network id as interface's id + net_id = intf_id + network = self.get_network(context, net_id) + subnet = self.get_subnet(context, subnet_id) + mapped_network = self._get_mapped_network_with_subnets(network) + mapped_subnet = self._map_state_and_status(subnet) + + data = { + 'id': intf_id, + "network": mapped_network, + "subnet": mapped_subnet + } + + return data + + def _extend_port_dict_binding(self, context, port): + cfg_vif_type = cfg.CONF.NOVA.vif_type.lower() + if not cfg_vif_type in (portbindings.VIF_TYPE_OVS, + portbindings.VIF_TYPE_IVS): + LOG.warning(_("Unrecognized vif_type in configuration " + "[%s]. Defaulting to ovs."), + cfg_vif_type) + cfg_vif_type = portbindings.VIF_TYPE_OVS + hostid = porttracker_db.get_port_hostid(context, port['id']) + if hostid: + port[portbindings.HOST_ID] = hostid + override = self._check_hostvif_override(hostid) + if override: + cfg_vif_type = override + port[portbindings.VIF_TYPE] = cfg_vif_type + + port[portbindings.CAPABILITIES] = { + portbindings.CAP_PORT_FILTER: + 'security-group' in self.supported_extension_aliases} + return port + + def _check_hostvif_override(self, hostid): + for v in cfg.CONF.NOVA.vif_types: + if hostid in getattr(cfg.CONF.NOVA, "node_override_vif_" + v, []): + return v + return False + + +class NeutronRestProxyV2(NeutronRestProxyV2Base, extradhcpopt_db.ExtraDhcpOptMixin, agentschedulers_db.DhcpAgentSchedulerDbMixin): @@ -443,28 +666,10 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # Include the BigSwitch Extensions path in the api_extensions neutron_extensions.append_api_extensions_path(extensions.__path__) - # 'servers' is the list of network controller REST end-points - # (used in order specified till one succeeds, and it is sticky - # till next failure). Use 'server_auth' to encode api-key - servers = cfg.CONF.RESTPROXY.servers - server_auth = cfg.CONF.RESTPROXY.server_auth - server_ssl = cfg.CONF.RESTPROXY.server_ssl - sync_data = cfg.CONF.RESTPROXY.sync_data - neutron_id = cfg.CONF.RESTPROXY.neutron_id self.add_meta_server_route = cfg.CONF.RESTPROXY.add_meta_server_route - timeout = cfg.CONF.RESTPROXY.server_timeout - if server_timeout is not None: - timeout = server_timeout - - # validate config - assert servers is not None, _('Servers not defined. Aborting plugin') - servers = tuple(s.rsplit(':', 1) for s in servers.split(',')) - servers = tuple((server, int(port)) for server, port in servers) - assert all(len(s) == 2 for s in servers), SYNTAX_ERROR_MESSAGE # init network ctrl connections - self.servers = ServerPool(servers, server_ssl, server_auth, neutron_id, - timeout, BASE_URI) + self.servers = ServerPool(server_timeout, BASE_URI) # init dhcp support self.topic = topics.PLUGIN @@ -482,7 +687,7 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, fanout=False) # Consume from all consumers in a thread self.conn.consume_in_thread() - if sync_data: + if cfg.CONF.RESTPROXY.sync_data: self._send_all_data() LOG.debug(_("NeutronRestProxyV2: initialization done")) @@ -516,19 +721,12 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, self._warn_on_state_status(network['network']) with context.session.begin(subtransactions=True): - # Validate args - tenant_id = self._get_tenant_id_for_create(context, - network["network"]) - # create network in DB new_net = super(NeutronRestProxyV2, self).create_network(context, network) self._process_l3_create(context, new_net, network['network']) - mapped_network = self._get_mapped_network_with_subnets(new_net, - context) - # create network on the network controller - self.servers.rest_create_network(tenant_id, mapped_network) + self._send_create_network(new_net, context) # return created network return new_net @@ -585,7 +783,6 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # Validate args orig_net = super(NeutronRestProxyV2, self).get_network(context, net_id) - tenant_id = orig_net["tenant_id"] filter = {'network_id': [net_id]} ports = self.get_ports(context, filters=filter) @@ -600,7 +797,7 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, with context.session.begin(subtransactions=True): ret_val = super(NeutronRestProxyV2, self).delete_network(context, net_id) - self.servers.rest_delete_network(tenant_id, net_id) + self._send_delete_network(orig_net, context) return ret_val def create_port(self, context, port): @@ -633,7 +830,6 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # Update DB in new session so exceptions rollback changes with context.session.begin(subtransactions=True): - port["port"]["admin_state_up"] = False dhcp_opts = port['port'].get(edo_ext.EXTRADHCPOPTS, []) new_port = super(NeutronRestProxyV2, self).create_port(context, port) @@ -647,7 +843,6 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, new_port = self._extend_port_dict_binding(context, new_port) net = super(NeutronRestProxyV2, self).get_network(context, new_port["network_id"]) - if self.add_meta_server_route: if new_port['device_owner'] == 'network:dhcp': destination = METADATA_SERVER_IP + '/32' @@ -655,28 +850,10 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # create on network ctrl mapped_port = self._map_state_and_status(new_port) - self.servers.rest_create_port(net, mapped_port) - - # connect device to network, if present - device_id = port["port"].get("device_id") - if device_id: - try: - self.servers.rest_plug_interface(net["tenant_id"], net["id"], - new_port, device_id) - except RemoteRestError: - with excutils.save_and_reraise_exception(): - port_update = {"port": {"status": "ERROR"}} - super(NeutronRestProxyV2, self).update_port( - context, - new_port["id"], - port_update - ) - # Set port state up and return that port - port_update = {"port": {"admin_state_up": True}} - new_port = super(NeutronRestProxyV2, self).update_port(context, - new_port["id"], - port_update) - return self._extend_port_dict_binding(context, new_port) + self.servers.rest_create_port(net["tenant_id"], + new_port["network_id"], + mapped_port) + return new_port def get_port(self, context, id, fields=None): with context.session.begin(subtransactions=True): @@ -732,39 +909,27 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, self).update_port(context, port_id, port) self._update_extra_dhcp_opts_on_port(context, port_id, port, new_port) + ctrl_update_required = False + old_host_id = porttracker_db.get_port_hostid(context, + orig_port['id']) if (portbindings.HOST_ID in port['port'] and 'id' in new_port): host_id = port['port'][portbindings.HOST_ID] porttracker_db.put_port_hostid(context, new_port['id'], host_id) - new_port = self._extend_port_dict_binding(context, new_port) + if old_host_id != host_id: + ctrl_update_required = True - # update on networl ctrl - mapped_port = self._map_state_and_status(new_port) - self.servers.rest_update_port(orig_port["tenant_id"], - orig_port["network_id"], - mapped_port, port_id) + if (new_port.get("device_id") != orig_port.get("device_id") and + orig_port.get("device_id")): + ctrl_update_required = True - if (new_port.get("device_id") != orig_port.get("device_id") and - orig_port.get("device_id")): - try: - self.servers.rest_unplug_interface(orig_port["tenant_id"], - orig_port["network_id"], - orig_port["id"]) - device_id = new_port.get("device_id") - if device_id: - self.rest_plug_interface(new_port["tenant_id"], - new_port["network_id"], - new_port, device_id) - - except RemoteRestError: - with excutils.save_and_reraise_exception(): - port_update = {"port": {"status": "ERROR"}} - super(NeutronRestProxyV2, self).update_port( - context, - new_port["id"], - port_update - ) + if ctrl_update_required: + new_port = self._extend_port_dict_binding(context, new_port) + mapped_port = self._map_state_and_status(new_port) + self.servers.rest_update_port(new_port["tenant_id"], + new_port["network_id"], + mapped_port) # return new_port return new_port @@ -788,28 +953,8 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, self.prevent_l3_port_deletion(context, port_id) with context.session.begin(subtransactions=True): self.disassociate_floatingips(context, port_id) - self._unplug_port(context, port_id) - # Separate transaction for delete in case unplug passes - # but delete fails on controller - with context.session.begin(subtransactions=True): super(NeutronRestProxyV2, self).delete_port(context, port_id) - def _unplug_port(self, context, port_id): - port = super(NeutronRestProxyV2, self).get_port(context, port_id) - tenant_id = port['tenant_id'] - net_id = port['network_id'] - if tenant_id == '': - net = super(NeutronRestProxyV2, self).get_network(context, net_id) - tenant_id = net['tenant_id'] - if port.get("device_id"): - self.servers.rest_unplug_interface(tenant_id, net_id, port_id) - # Port should transition to error state now that it's unplugged - # but not yet deleted - port_update = {"port": {"status": "ERROR"}} - super(NeutronRestProxyV2, self).update_port(context, - port_id, - port_update) - def _delete_port(self, context, port_id): port = super(NeutronRestProxyV2, self).get_port(context, port_id) tenant_id = port['tenant_id'] @@ -1086,69 +1231,6 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, # networks are detected, which isn't supported by the Plugin LOG.error(_("NeutronRestProxyV2: too many external networks")) - def _send_all_data(self): - """Pushes all data to network ctrl (networks/ports, ports/attachments). - - This gives the controller an option to re-sync it's persistent store - with neutron's current view of that data. - """ - admin_context = qcontext.get_admin_context() - networks = [] - routers = [] - - all_networks = super(NeutronRestProxyV2, - self).get_networks(admin_context) or [] - for net in all_networks: - mapped_network = self._get_mapped_network_with_subnets(net) - net_fl_ips = self._get_network_with_floatingips(mapped_network) - - ports = [] - net_filter = {'network_id': [net.get('id')]} - net_ports = super(NeutronRestProxyV2, - self).get_ports(admin_context, - filters=net_filter) or [] - for port in net_ports: - mapped_port = self._map_state_and_status(port) - mapped_port['attachment'] = { - 'id': port.get('device_id'), - 'mac': port.get('mac_address'), - } - ports.append(mapped_port) - net_fl_ips['ports'] = ports - - networks.append(net_fl_ips) - - all_routers = super(NeutronRestProxyV2, - self).get_routers(admin_context) or [] - for router in all_routers: - interfaces = [] - mapped_router = self._map_state_and_status(router) - router_filter = { - 'device_owner': ["network:router_interface"], - 'device_id': [router.get('id')] - } - router_ports = super(NeutronRestProxyV2, - self).get_ports(admin_context, - filters=router_filter) or [] - for port in router_ports: - net_id = port.get('network_id') - subnet_id = port['fixed_ips'][0]['subnet_id'] - intf_details = self._get_router_intf_details(admin_context, - net_id, - subnet_id) - interfaces.append(intf_details) - mapped_router['interfaces'] = interfaces - - routers.append(mapped_router) - - resource = '/topology' - data = { - 'networks': networks, - 'routers': routers, - } - errstr = _("Unable to update remote topology: %s") - return self.servers.rest_action('PUT', resource, data, errstr) - def _add_host_route(self, context, destination, port): subnet = {} for fixed_ip in port['fixed_ips']: @@ -1165,131 +1247,3 @@ class NeutronRestProxyV2(db_base_plugin_v2.NeutronDbPluginV2, LOG.debug(_("Adding host route: ")) LOG.debug(_("Destination:%(dst)s nexthop:%(next)s"), {'dst': destination, 'next': nexthop}) - - def _get_network_with_floatingips(self, network, context=None): - if context is None: - context = qcontext.get_admin_context() - - net_id = network['id'] - net_filter = {'floating_network_id': [net_id]} - fl_ips = super(NeutronRestProxyV2, - self).get_floatingips(context, - filters=net_filter) or [] - network['floatingips'] = fl_ips - - return network - - def _get_all_subnets_json_for_network(self, net_id, context=None): - if context is None: - context = qcontext.get_admin_context() - # start a sub-transaction to avoid breaking parent transactions - with context.session.begin(subtransactions=True): - subnets = self._get_subnets_by_network(context, - net_id) - subnets_details = [] - if subnets: - for subnet in subnets: - subnet_dict = self._make_subnet_dict(subnet) - mapped_subnet = self._map_state_and_status(subnet_dict) - subnets_details.append(mapped_subnet) - - return subnets_details - - def _get_mapped_network_with_subnets(self, network, context=None): - # if context is not provided, admin context is used - if context is None: - context = qcontext.get_admin_context() - network = self._map_state_and_status(network) - subnets = self._get_all_subnets_json_for_network(network['id'], - context) - network['subnets'] = subnets - for subnet in (subnets or []): - if subnet['gateway_ip']: - # FIX: For backward compatibility with wire protocol - network['gateway'] = subnet['gateway_ip'] - break - else: - network['gateway'] = '' - network[external_net.EXTERNAL] = self._network_is_external( - context, network['id']) - - return network - - def _send_update_network(self, network, context): - net_id = network['id'] - tenant_id = network['tenant_id'] - # update network on network controller - mapped_network = self._get_mapped_network_with_subnets(network, - context) - net_fl_ips = self._get_network_with_floatingips(mapped_network, - context) - self.servers.rest_update_network(tenant_id, net_id, net_fl_ips) - - def _map_state_and_status(self, resource): - resource = copy.copy(resource) - - resource['state'] = ('UP' if resource.pop('admin_state_up', - True) else 'DOWN') - - if 'status' in resource: - del resource['status'] - - return resource - - def _warn_on_state_status(self, resource): - if resource.get('admin_state_up', True) is False: - LOG.warning(_("Setting admin_state_up=False is not supported" - " in this plugin version. Ignoring setting for " - "resource: %s"), resource) - - if 'status' in resource: - if resource['status'] is not const.NET_STATUS_ACTIVE: - LOG.warning(_("Operational status is internally set by the" - " plugin. Ignoring setting status=%s."), - resource['status']) - - def _get_router_intf_details(self, context, intf_id, subnet_id): - - # we will use the network id as interface's id - net_id = intf_id - network = super(NeutronRestProxyV2, self).get_network(context, - net_id) - subnet = super(NeutronRestProxyV2, self).get_subnet(context, - subnet_id) - mapped_network = self._get_mapped_network_with_subnets(network) - mapped_subnet = self._map_state_and_status(subnet) - - data = { - 'id': intf_id, - "network": mapped_network, - "subnet": mapped_subnet - } - - return data - - def _extend_port_dict_binding(self, context, port): - cfg_vif_type = cfg.CONF.NOVA.vif_type.lower() - if not cfg_vif_type in (portbindings.VIF_TYPE_OVS, - portbindings.VIF_TYPE_IVS): - LOG.warning(_("Unrecognized vif_type in configuration " - "[%s]. Defaulting to ovs. "), - cfg_vif_type) - cfg_vif_type = portbindings.VIF_TYPE_OVS - hostid = porttracker_db.get_port_hostid(context, - port['id']) - if hostid: - override = self._check_hostvif_override(hostid) - if override: - cfg_vif_type = override - port[portbindings.VIF_TYPE] = cfg_vif_type - - port[portbindings.CAPABILITIES] = { - portbindings.CAP_PORT_FILTER: - 'security-group' in self.supported_extension_aliases} - return port - - def _check_hostvif_override(self, hostid): - for v in cfg.CONF.NOVA.vif_types: - if hostid in getattr(cfg.CONF.NOVA, "node_override_vif_" + v, []): - return v - return False diff --git a/neutron/plugins/ml2/drivers/mech_bigswitch/__init__.py b/neutron/plugins/ml2/drivers/mech_bigswitch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py new file mode 100644 index 000000000..d1eb08ec0 --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 Big Switch Networks, Inc. +# 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: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +from oslo.config import cfg + +from neutron import context as ctx +from neutron.extensions import portbindings +from neutron.openstack.common import log +from neutron.plugins.bigswitch.db import porttracker_db +from neutron.plugins.bigswitch.plugin import NeutronRestProxyV2Base +from neutron.plugins.bigswitch.plugin import ServerPool +from neutron.plugins.ml2 import driver_api as api + + +LOG = log.getLogger(__name__) + + +class BigSwitchMechanismDriver(NeutronRestProxyV2Base, + api.MechanismDriver): + + """Mechanism Driver for Big Switch Networks Controller. + + This driver relays the network create, update, delete + operations to the Big Switch Controller. + """ + + def initialize(self, server_timeout=None): + LOG.debug(_('Initializing driver')) + + # backend doesn't support bulk operations yet + self.native_bulk_support = False + + # init network ctrl connections + self.servers = ServerPool(server_timeout) + self.segmentation_types = ', '.join(cfg.CONF.ml2.type_drivers) + LOG.debug(_("Initialization done")) + + def create_network_postcommit(self, context): + # create network on the network controller + self._send_create_network(context.current) + + def update_network_postcommit(self, context): + # update network on the network controller + self._send_update_network(context.current) + + def delete_network_postcommit(self, context): + # delete network on the network controller + self._send_delete_network(context.current) + + def create_port_postcommit(self, context): + # create port on the network controller + port = self._prepare_port_for_controller(context) + if port: + self.servers.rest_create_port(port["network"]["tenant_id"], + port["network"]["id"], port) + + def update_port_postcommit(self, context): + # update port on the network controller + port = self._prepare_port_for_controller(context) + if port: + self.servers.rest_update_port(port["network"]["tenant_id"], + port["network"]["id"], port) + + def delete_port_postcommit(self, context): + # delete port on the network controller + port = context.current + net = context.network.current + self.servers.rest_delete_port(net["tenant_id"], net["id"], port['id']) + + def _prepare_port_for_controller(self, context): + port = context.current + net = context.network.current + port['network'] = net + port['binding_host'] = context._binding.host + actx = ctx.get_admin_context() + if (portbindings.HOST_ID in port and 'id' in port): + host_id = port[portbindings.HOST_ID] + porttracker_db.put_port_hostid(actx, port['id'], host_id) + else: + host_id = '' + prepped_port = self._extend_port_dict_binding(actx, port) + prepped_port = self._map_state_and_status(prepped_port) + if (portbindings.HOST_ID not in prepped_port or + prepped_port[portbindings.HOST_ID] == ''): + # in ML2, controller doesn't care about ports without + # the host_id set + return False + return prepped_port diff --git a/neutron/tests/unit/bigswitch/test_restproxy_plugin.py b/neutron/tests/unit/bigswitch/test_restproxy_plugin.py index 6fe30e6a8..4f55a2a76 100644 --- a/neutron/tests/unit/bigswitch/test_restproxy_plugin.py +++ b/neutron/tests/unit/bigswitch/test_restproxy_plugin.py @@ -22,7 +22,6 @@ import webob.exc from neutron import context from neutron.extensions import portbindings from neutron.manager import NeutronManager -from neutron.plugins.bigswitch.plugin import RemoteRestError from neutron.tests.unit import _test_extension_portbindings as test_bindings from neutron.tests.unit.bigswitch import fake_server from neutron.tests.unit.bigswitch import test_base @@ -33,9 +32,11 @@ import neutron.tests.unit.test_db_plugin as test_plugin class BigSwitchProxyPluginV2TestCase(test_base.BigSwitchTestBase, test_plugin.NeutronDbPluginV2TestCase): - def setUp(self): + def setUp(self, plugin_name=None): self.setup_config_files() self.setup_patches() + if plugin_name: + self._plugin_name = plugin_name super(BigSwitchProxyPluginV2TestCase, self).setUp(self._plugin_name) @@ -86,29 +87,12 @@ class TestBigSwitchProxyPortsV2(test_plugin.TestPortsV2, #failure to create should result in no ports self.assertEqual(0, len(ports)) - def test_rollback_on_port_attach(self): - with self.network() as n: - plugin_obj = NeutronManager.get_plugin() - with patch.object(plugin_obj.servers, - 'rest_plug_interface') as mock_plug_interface: - mock_plug_interface.side_effect = RemoteRestError( - reason='fake error') - kwargs = {'device_id': 'somedevid', - 'tenant_id': n['network']['tenant_id']} - self._create_port('json', n['network']['id'], - expected_code= - webob.exc.HTTPInternalServerError.code, - **kwargs) - port = self._get_ports(n['network']['id'])[0] - # Attachment failure should leave created port in error state - self.assertEqual('ERROR', port['status']) - self._delete('ports', port['id']) - def test_rollback_for_port_update(self): with self.network() as n: - with self.port(network_id=n['network']['id']) as port: + with self.port(network_id=n['network']['id'], + device_id='66') as port: port = self._get_ports(n['network']['id'])[0] - data = {'port': {'name': 'aNewName'}} + data = {'port': {'name': 'aNewName', 'device_id': '99'}} self.httpPatch = patch('httplib.HTTPConnection', create=True, new=fake_server.HTTPConnectionMock500) self.httpPatch.start() @@ -120,7 +104,7 @@ class TestBigSwitchProxyPortsV2(test_plugin.TestPortsV2, # name should have stayed the same self.assertEqual(port['name'], uport['name']) - def test_rollback_for_port_detach(self): + def test_rollback_for_port_delete(self): with self.network() as n: with self.port(network_id=n['network']['id'], device_id='somedevid') as port: @@ -134,22 +118,6 @@ class TestBigSwitchProxyPortsV2(test_plugin.TestPortsV2, port = self._get_ports(n['network']['id'])[0] self.assertEqual('ACTIVE', port['status']) - def test_rollback_for_port_delete(self): - with self.network() as n: - with self.port(network_id=n['network']['id'], - device_id='somdevid') as port: - plugin_obj = NeutronManager.get_plugin() - with patch.object(plugin_obj.servers, - 'rest_delete_port' - ) as mock_plug_interface: - mock_plug_interface.side_effect = RemoteRestError( - reason='fake error') - self._delete('ports', port['port']['id'], - expected_code= - webob.exc.HTTPInternalServerError.code) - port = self._get_ports(n['network']['id'])[0] - self.assertEqual('ERROR', port['status']) - class TestBigSwitchProxyPortsV2IVS(test_plugin.TestPortsV2, BigSwitchProxyPluginV2TestCase, diff --git a/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py b/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py new file mode 100644 index 000000000..93e52f4d6 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/test_bigswitch_mech.py @@ -0,0 +1,95 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2014 Big Switch Networks, Inc. +# 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. + +import webob.exc + +from neutron.extensions import portbindings +from neutron.plugins.ml2 import config as ml2_config +from neutron.plugins.ml2.drivers import type_vlan as vlan_config +import neutron.tests.unit.bigswitch.test_restproxy_plugin as trp +from neutron.tests.unit.ml2.test_ml2_plugin import PLUGIN_NAME as ML2_PLUGIN +from neutron.tests.unit import test_db_plugin + +PHYS_NET = 'physnet1' +VLAN_START = 1000 +VLAN_END = 1100 + + +class TestBigSwitchMechDriverBase(trp.BigSwitchProxyPluginV2TestCase): + + def setUp(self): + # Configure the ML2 mechanism drivers and network types + ml2_opts = { + 'mechanism_drivers': ['bigswitch'], + 'tenant_network_types': ['vlan'], + } + for opt, val in ml2_opts.items(): + ml2_config.cfg.CONF.set_override(opt, val, 'ml2') + self.addCleanup(ml2_config.cfg.CONF.reset) + + # Configure the ML2 VLAN parameters + phys_vrange = ':'.join([PHYS_NET, str(VLAN_START), str(VLAN_END)]) + vlan_config.cfg.CONF.set_override('network_vlan_ranges', + [phys_vrange], + 'ml2_type_vlan') + self.addCleanup(vlan_config.cfg.CONF.reset) + super(TestBigSwitchMechDriverBase, + self).setUp(ML2_PLUGIN) + + +class TestBigSwitchMechDriverNetworksV2(test_db_plugin.TestNetworksV2, + TestBigSwitchMechDriverBase): + pass + + +class TestBigSwitchMechDriverPortsV2(test_db_plugin.TestPortsV2, + TestBigSwitchMechDriverBase): + + VIF_TYPE = portbindings.VIF_TYPE_OVS + + def setUp(self): + super(TestBigSwitchMechDriverPortsV2, self).setUp() + self.port_create_status = 'DOWN' + + def test_update_port_status_build(self): + with self.port() as port: + self.assertEqual(port['port']['status'], 'DOWN') + self.assertEqual(self.port_create_status, 'DOWN') + + # exercise the host_id tracking code + def test_port_vif_details(self): + kwargs = {'name': 'name', 'binding:host_id': 'ivshost', + 'device_id': 'override_dev'} + with self.port(**kwargs) as port: + self.assertEqual(port['port']['binding:vif_type'], + portbindings.VIF_TYPE_IVS) + kwargs = {'name': 'name2', 'binding:host_id': 'someotherhost', + 'device_id': 'other_dev'} + with self.port(**kwargs) as port: + self.assertEqual(port['port']['binding:vif_type'], self.VIF_TYPE) + + def _make_port(self, fmt, net_id, expected_res_status=None, arg_list=None, + **kwargs): + arg_list = arg_list or () + arg_list += ('binding:host_id', ) + res = self._create_port(fmt, net_id, expected_res_status, + arg_list, **kwargs) + # Things can go wrong - raise HTTP exc with res code only + # so it can be caught by unit tests + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + return self.deserialize(fmt, res) diff --git a/setup.cfg b/setup.cfg index d8c21da15..8b6ab1657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ data_files = etc/neutron/plugins/ml2/ml2_conf.ini etc/neutron/plugins/ml2/ml2_conf_arista.ini etc/neutron/plugins/ml2/ml2_conf_cisco.ini + etc/neutron/plugins/bigswitch/restproxy.ini etc/neutron/plugins/mlnx = etc/neutron/plugins/mlnx/mlnx_conf.ini etc/neutron/plugins/nec = etc/neutron/plugins/nec/nec.ini etc/neutron/plugins/nicira = etc/neutron/plugins/nicira/nvp.ini @@ -158,6 +159,7 @@ neutron.ml2.mechanism_drivers = arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver cisco_nexus = neutron.plugins.ml2.drivers.cisco.mech_cisco_nexus:CiscoNexusMechanismDriver l2population = neutron.plugins.ml2.drivers.l2pop.mech_driver:L2populationMechanismDriver + bigswitch = neutron.plugins.ml2.drivers.mech_bigswitch.driver:BigSwitchMechanismDriver [build_sphinx] all_files = 1 -- 2.45.2