From: Praneet Bachheti Date: Thu, 29 May 2014 23:11:39 +0000 (-0700) Subject: Opencontrail plug-in implementation for core resources X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=1a1561f8eb883e9200248c9c41d3bcc992edac8c;p=openstack-build%2Fneutron-build.git Opencontrail plug-in implementation for core resources Adds Opencontrail plug-in implementation with unit tests This patch has no dependency on any other blueprints The link below describes how to install VIF driver for opencontrail https://github.com/Juniper/contrail-controller/wiki/ OpenContrail-bring-up-and-provisioning - The contrail_plugin_core.py is the main interface for neutron common infrastructure. It relays API requests to the opencontrail controller DocImpact Change-Id: I501bf669b2a999a171f9a3ee3e9893d4ead50e3b Implements: blueprint juniper-plugin-with-extensions --- diff --git a/etc/neutron/plugins/opencontrail/contrailplugin.ini b/etc/neutron/plugins/opencontrail/contrailplugin.ini new file mode 100644 index 000000000..629f1fc4e --- /dev/null +++ b/etc/neutron/plugins/opencontrail/contrailplugin.ini @@ -0,0 +1,26 @@ +# OpenContrail is an Apache 2.0-licensed project that is built using +# standards-based protocols and provides all the necessary components for +# network virtualization–SDN controller, virtual router, analytics engine, +# and published northbound APIs +# For more information visit: http://opencontrail.org + +# Opencontrail plugin specific configuration +[CONTRAIL] +# (StrOpt) IP address to connect to opencontrail controller. +# Uncomment this line for specifying the IP address of the opencontrail +# Api-Server. +# Default value is local host(127.0.0.1). +# api_server_ip='127.0.0.1' + +# (IntOpt) port to connect to opencontrail controller. +# Uncomment this line for the specifying the Port of the opencontrail +# Api-Server. +# Default value is 8082 +# api_server_port=8082 + +# (DictOpt) enable opencontrail extensions +# Opencontrail in future would support extension such as ipam, policy, +# these extensions can be configured as shown below. Plugin will then +# load the specified extensions. +# Default value is None, it wont load any extension +# contrail_extensions=ipam:,policy: diff --git a/neutron/extensions/portbindings.py b/neutron/extensions/portbindings.py index 23ffbc948..4e199e500 100644 --- a/neutron/extensions/portbindings.py +++ b/neutron/extensions/portbindings.py @@ -62,12 +62,14 @@ VIF_TYPE_MIDONET = 'midonet' VIF_TYPE_MLNX_DIRECT = 'mlnx_direct' VIF_TYPE_MLNX_HOSTDEV = 'hostdev' VIF_TYPE_HW_VEB = 'hw_veb' +VIF_TYPE_VROUTER = 'vrouter' VIF_TYPE_OTHER = 'other' VIF_TYPES = [VIF_TYPE_UNBOUND, VIF_TYPE_BINDING_FAILED, VIF_TYPE_OVS, VIF_TYPE_IVS, VIF_TYPE_BRIDGE, VIF_TYPE_802_QBG, VIF_TYPE_802_QBH, VIF_TYPE_HYPERV, VIF_TYPE_MIDONET, VIF_TYPE_MLNX_DIRECT, VIF_TYPE_MLNX_HOSTDEV, VIF_TYPE_HW_VEB, - VIF_TYPE_DVS, VIF_TYPE_OTHER, VIF_TYPE_DISTRIBUTED] + VIF_TYPE_DVS, VIF_TYPE_OTHER, VIF_TYPE_DISTRIBUTED, + VIF_TYPE_VROUTER] VNIC_NORMAL = 'normal' VNIC_DIRECT = 'direct' diff --git a/neutron/plugins/opencontrail/__init__.py b/neutron/plugins/opencontrail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/opencontrail/common/__init__.py b/neutron/plugins/opencontrail/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/opencontrail/common/exceptions.py b/neutron/plugins/opencontrail/common/exceptions.py new file mode 100644 index 000000000..34aeadb97 --- /dev/null +++ b/neutron/plugins/opencontrail/common/exceptions.py @@ -0,0 +1,40 @@ +# Copyright 2014 Juniper Networks. 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. +# + +from neutron.common import exceptions as exc + + +class ContrailError(exc.NeutronException): + message = '%(msg)s' + + +class ContrailNotFoundError(exc.NotFound): + message = '%(msg)s' + + +class ContrailConflictError(exc.Conflict): + message = '%(msg)s' + + +class ContrailBadRequestError(exc.BadRequest): + message = '%(msg)s' + + +class ContrailServiceUnavailableError(exc.ServiceUnavailable): + message = '%(msg)s' + + +class ContrailNotAuthorizedError(exc.NotAuthorized): + message = '%(msg)s' diff --git a/neutron/plugins/opencontrail/contrail_plugin.py b/neutron/plugins/opencontrail/contrail_plugin.py new file mode 100644 index 000000000..511c68463 --- /dev/null +++ b/neutron/plugins/opencontrail/contrail_plugin.py @@ -0,0 +1,622 @@ +# Copyright 2014 Juniper Networks. 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. +# + +from oslo.config import cfg +import requests + +from neutron.api.v2 import attributes as attr +from neutron.common import exceptions as exc +from neutron.db import portbindings_base +from neutron.extensions import external_net +from neutron.extensions import portbindings +from neutron.extensions import securitygroup +from neutron import neutron_plugin_base_v2 +from neutron.openstack.common import jsonutils +from neutron.openstack.common import log as logging +from neutron.plugins.opencontrail.common import exceptions as c_exc + + +LOG = logging.getLogger(__name__) + +opencontrail_opts = [ + cfg.StrOpt('api_server_ip', default='127.0.0.1', + help='IP address to connect to opencontrail controller'), + cfg.IntOpt('api_server_port', default=8082, + help='Port to connect to opencontrail controller'), +] + +cfg.CONF.register_opts(opencontrail_opts, 'CONTRAIL') + +CONTRAIL_EXCEPTION_MAP = { + requests.codes.not_found: c_exc.ContrailNotFoundError, + requests.codes.conflict: c_exc.ContrailConflictError, + requests.codes.bad_request: c_exc.ContrailBadRequestError, + requests.codes.service_unavailable: c_exc.ContrailServiceUnavailableError, + requests.codes.unauthorized: c_exc.ContrailNotAuthorizedError, + requests.codes.internal_server_error: c_exc.ContrailError, +} + + +class NeutronPluginContrailCoreV2(neutron_plugin_base_v2.NeutronPluginBaseV2, + securitygroup.SecurityGroupPluginBase, + portbindings_base.PortBindingBaseMixin, + external_net.External_net): + + supported_extension_aliases = ["security-group", "router", + "port-security", "binding", "agent", + "quotas", "external-net"] + PLUGIN_URL_PREFIX = '/neutron' + __native_bulk_support = False + + def __init__(self): + """Initialize the plugin class.""" + + super(NeutronPluginContrailCoreV2, self).__init__() + portbindings_base.register_port_dict_function() + self.base_binding_dict = self._get_base_binding_dict() + + def _get_base_binding_dict(self): + """return VIF type and details.""" + + binding = { + portbindings.VIF_TYPE: portbindings.VIF_TYPE_VROUTER, + portbindings.VIF_DETAILS: { + # TODO(praneetb): Replace with new VIF security details + portbindings.CAP_PORT_FILTER: + 'security-group' in self.supported_extension_aliases + } + } + return binding + + def _request_api_server(self, url, data=None, headers=None): + """Send received request to api server.""" + + return requests.post(url, data=data, headers=headers) + + def _relay_request(self, url_path, data=None): + """Send received request to api server.""" + + url = "http://%s:%d%s" % (cfg.CONF.CONTRAIL.api_server_ip, + cfg.CONF.CONTRAIL.api_server_port, + url_path) + + return self._request_api_server( + url, data=data, headers={'Content-type': 'application/json'}) + + def _request_backend(self, context, data_dict, obj_name, action): + """Relays request to the controller.""" + + context_dict = self._encode_context(context, action, obj_name) + data = jsonutils.dumps({'context': context_dict, 'data': data_dict}) + + url_path = "%s/%s" % (self.PLUGIN_URL_PREFIX, obj_name) + response = self._relay_request(url_path, data=data) + if response.content: + return response.status_code, response.json() + else: + return response.status_code, response.content + + def _encode_context(self, context, operation, apitype): + """Encode the context to be sent to the controller.""" + + cdict = {'user_id': getattr(context, 'user_id', ''), + 'is_admin': getattr(context, 'is_admin', False), + 'operation': operation, + 'type': apitype, + 'tenant_id': getattr(context, 'tenant_id', None)} + if context.roles: + cdict['roles'] = context.roles + if context.tenant: + cdict['tenant'] = context.tenant + return cdict + + def _encode_resource(self, resource_id=None, resource=None, fields=None, + filters=None): + """Encode a resource to be sent to the controller.""" + + resource_dict = {} + if resource_id: + resource_dict['id'] = resource_id + if resource: + resource_dict['resource'] = resource + resource_dict['filters'] = filters + resource_dict['fields'] = fields + return resource_dict + + def _prune(self, resource_dict, fields): + """Prune the resource dictionary based in the fields.""" + + if fields: + return dict(((key, item) for key, item in resource_dict.items() + if key in fields)) + return resource_dict + + def _transform_response(self, status_code, info=None, obj_name=None, + fields=None): + """Transform the response for a Resource API.""" + + if status_code == requests.codes.ok: + if not isinstance(info, list): + return self._prune(info, fields) + else: + return [self._prune(items, fields) for items in info] + self._raise_contrail_error(status_code, info, obj_name) + + def _raise_contrail_error(self, status_code, info, obj_name): + """Raises an error in handling of a Resource. + + This method converts return error code into neutron exception. + """ + + if status_code == requests.codes.bad_request: + raise c_exc.ContrailBadRequestError( + msg=info['message'], resource=obj_name) + error_class = CONTRAIL_EXCEPTION_MAP.get(status_code, + c_exc.ContrailError) + raise error_class(msg=info['message']) + + def _create_resource(self, res_type, context, res_data): + """Create a resource in API server. + + This method encodes neutron model, and sends it to the + contrail api server. + """ + + for key, value in res_data[res_type].items(): + if value == attr.ATTR_NOT_SPECIFIED: + res_data[res_type][key] = None + + res_dict = self._encode_resource(resource=res_data[res_type]) + status_code, res_info = self._request_backend(context, res_dict, + res_type, 'CREATE') + res_dicts = self._transform_response(status_code, info=res_info, + obj_name=res_type) + LOG.debug("create_%(res_type)s(): %(res_dicts)s", + {'res_type': res_type, 'res_dicts': res_dicts}) + + return res_dicts + + def _get_resource(self, res_type, context, res_id, fields): + """Get a resource from API server. + + This method gets a resource from the contrail api server + """ + + res_dict = self._encode_resource(resource_id=res_id, fields=fields) + status_code, res_info = self._request_backend(context, res_dict, + res_type, 'READ') + res_dicts = self._transform_response(status_code, info=res_info, + fields=fields, obj_name=res_type) + LOG.debug("get_%(res_type)s(): %(res_dicts)s", + {'res_type': res_type, 'res_dicts': res_dicts}) + + return res_dicts + + def _update_resource(self, res_type, context, res_id, res_data): + """Update a resource in API server. + + This method updates a resource in the contrail api server + """ + + res_dict = self._encode_resource(resource_id=res_id, + resource=res_data[res_type]) + status_code, res_info = self._request_backend(context, res_dict, + res_type, 'UPDATE') + res_dicts = self._transform_response(status_code, info=res_info, + obj_name=res_type) + LOG.debug("update_%(res_type)s(): %(res_dicts)s", + {'res_type': res_type, 'res_dicts': res_dicts}) + + return res_dicts + + def _delete_resource(self, res_type, context, res_id): + """Delete a resource in API server + + This method deletes a resource in the contrail api server + """ + + res_dict = self._encode_resource(resource_id=res_id) + LOG.debug("delete_%(res_type)s(): %(res_id)s", + {'res_type': res_type, 'res_id': res_id}) + status_code, res_info = self._request_backend(context, res_dict, + res_type, 'DELETE') + if status_code != requests.codes.ok: + self._raise_contrail_error(status_code, info=res_info, + obj_name=res_type) + + def _list_resource(self, res_type, context, filters, fields): + """Get the list of a Resource.""" + + res_dict = self._encode_resource(filters=filters, fields=fields) + status_code, res_info = self._request_backend(context, res_dict, + res_type, 'READALL') + res_dicts = self._transform_response(status_code, info=res_info, + fields=fields, obj_name=res_type) + LOG.debug( + "get_%(res_type)s(): filters: %(filters)r data: %(res_dicts)r", + {'res_type': res_type, 'filters': filters, + 'res_dicts': res_dicts}) + + return res_dicts + + def _count_resource(self, res_type, context, filters): + """Get the count of a Resource.""" + + res_dict = self._encode_resource(filters=filters) + _, res_count = self._request_backend(context, res_dict, res_type, + 'READCOUNT') + LOG.debug("get_%(res_type)s_count(): %(res_count)r", + {'res_type': res_type, 'res_count': res_count}) + return res_count + + def _get_network(self, context, res_id, fields=None): + """Get the attributes of a Virtual Network.""" + + return self._get_resource('network', context, res_id, fields) + + def create_network(self, context, network): + """Creates a new Virtual Network.""" + + return self._create_resource('network', context, network) + + def get_network(self, context, network_id, fields=None): + """Get the attributes of a particular Virtual Network.""" + + return self._get_network(context, network_id, fields) + + def update_network(self, context, network_id, network): + """Updates the attributes of a particular Virtual Network.""" + + return self._update_resource('network', context, network_id, + network) + + def delete_network(self, context, network_id): + """Deletes the network with the specified network identifier.""" + + self._delete_resource('network', context, network_id) + + def get_networks(self, context, filters=None, fields=None): + """Get the list of Virtual Networks.""" + + return self._list_resource('network', context, filters, + fields) + + def get_networks_count(self, context, filters=None): + """Get the count of Virtual Network.""" + + networks_count = self._count_resource('network', context, filters) + return networks_count['count'] + + def create_subnet(self, context, subnet): + """Creates a new subnet, and assigns it a symbolic name.""" + + if subnet['subnet']['gateway_ip'] is None: + subnet['subnet']['gateway_ip'] = '0.0.0.0' + + if subnet['subnet']['host_routes'] != attr.ATTR_NOT_SPECIFIED: + if (len(subnet['subnet']['host_routes']) > + cfg.CONF.max_subnet_host_routes): + raise exc.HostRoutesExhausted(subnet_id=subnet[ + 'subnet'].get('id', _('new subnet')), + quota=cfg.CONF.max_subnet_host_routes) + + subnet_created = self._create_resource('subnet', context, subnet) + return self._make_subnet_dict(subnet_created) + + def _make_subnet_dict(self, subnet): + """Fixes subnet attributes.""" + + if subnet.get('gateway_ip') == '0.0.0.0': + subnet['gateway_ip'] = None + return subnet + + def _get_subnet(self, context, subnet_id, fields=None): + """Get the attributes of a subnet.""" + + subnet = self._get_resource('subnet', context, subnet_id, fields) + return self._make_subnet_dict(subnet) + + def get_subnet(self, context, subnet_id, fields=None): + """Get the attributes of a particular subnet.""" + + return self._get_subnet(context, subnet_id, fields) + + def update_subnet(self, context, subnet_id, subnet): + """Updates the attributes of a particular subnet.""" + + subnet = self._update_resource('subnet', context, subnet_id, subnet) + return self._make_subnet_dict(subnet) + + def delete_subnet(self, context, subnet_id): + """ + Deletes the subnet with the specified subnet identifier + belonging to the specified tenant. + """ + + self._delete_resource('subnet', context, subnet_id) + + def get_subnets(self, context, filters=None, fields=None): + """Get the list of subnets.""" + + return [self._make_subnet_dict(s) + for s in self._list_resource( + 'subnet', context, filters, fields)] + + def get_subnets_count(self, context, filters=None): + """Get the count of subnets.""" + + subnets_count = self._count_resource('subnet', context, filters) + return subnets_count['count'] + + def _make_port_dict(self, port, fields=None): + """filters attributes of a port based on fields.""" + + if not fields: + port.update(self.base_binding_dict) + else: + for key in self.base_binding_dict: + if key in fields: + port.update(self.base_binding_dict[key]) + return port + + def _get_port(self, context, res_id, fields=None): + """Get the attributes of a port.""" + + port = self._get_resource('port', context, res_id, fields) + return self._make_port_dict(port, fields) + + def _update_ips_for_port(self, context, original_ips, new_ips): + """Add or remove IPs from the port.""" + + # These ips are still on the port and haven't been removed + prev_ips = [] + + # the new_ips contain all of the fixed_ips that are to be updated + if len(new_ips) > cfg.CONF.max_fixed_ips_per_port: + msg = _('Exceeded maximim amount of fixed ips per port') + raise exc.InvalidInput(error_message=msg) + + # Remove all of the intersecting elements + for original_ip in original_ips[:]: + for new_ip in new_ips[:]: + if ('ip_address' in new_ip and + original_ip['ip_address'] == new_ip['ip_address']): + original_ips.remove(original_ip) + new_ips.remove(new_ip) + prev_ips.append(original_ip) + + return new_ips, prev_ips + + def create_port(self, context, port): + """Creates a port on the specified Virtual Network.""" + + port = self._create_resource('port', context, port) + return self._make_port_dict(port) + + def get_port(self, context, port_id, fields=None): + """Get the attributes of a particular port.""" + + return self._get_port(context, port_id, fields) + + def update_port(self, context, port_id, port): + """Updates a port. + + Updates the attributes of a port on the specified Virtual + Network. + """ + + if 'fixed_ips' in port['port']: + original = self._get_port(context, port_id) + added_ips, prev_ips = self._update_ips_for_port( + context, original['fixed_ips'], port['port']['fixed_ips']) + port['port']['fixed_ips'] = prev_ips + added_ips + + port = self._update_resource('port', context, port_id, port) + return self._make_port_dict(port) + + def delete_port(self, context, port_id): + """Deletes a port. + + Deletes a port on a specified Virtual Network, + if the port contains a remote interface attachment, + the remote interface is first un-plugged and then the port + is deleted. + """ + + self._delete_resource('port', context, port_id) + + def get_ports(self, context, filters=None, fields=None): + """Get all ports. + + Retrieves all port identifiers belonging to the + specified Virtual Network with the specfied filter. + """ + + return [self._make_port_dict(p, fields) + for p in self._list_resource('port', context, filters, fields)] + + def get_ports_count(self, context, filters=None): + """Get the count of ports.""" + + ports_count = self._count_resource('port', context, filters) + return ports_count['count'] + + # Router API handlers + def create_router(self, context, router): + """Creates a router. + + Creates a new Logical Router, and assigns it + a symbolic name. + """ + + return self._create_resource('router', context, router) + + def get_router(self, context, router_id, fields=None): + """Get the attributes of a router.""" + + return self._get_resource('router', context, router_id, fields) + + def update_router(self, context, router_id, router): + """Updates the attributes of a router.""" + + return self._update_resource('router', context, router_id, + router) + + def delete_router(self, context, router_id): + """Deletes a router.""" + + self._delete_resource('router', context, router_id) + + def get_routers(self, context, filters=None, fields=None): + """Retrieves all router identifiers.""" + + return self._list_resource('router', context, filters, fields) + + def get_routers_count(self, context, filters=None): + """Get the count of routers.""" + + routers_count = self._count_resource('router', context, filters) + return routers_count['count'] + + def _validate_router_interface_request(self, interface_info): + """Validates parameters to the router interface requests.""" + + port_id_specified = interface_info and 'port_id' in interface_info + subnet_id_specified = interface_info and 'subnet_id' in interface_info + if not (port_id_specified or subnet_id_specified): + msg = _("Either subnet_id or port_id must be specified") + raise exc.BadRequest(resource='router', msg=msg) + + def add_router_interface(self, context, router_id, interface_info): + """Add interface to a router.""" + + self._validate_router_interface_request(interface_info) + + if 'port_id' in interface_info: + if 'subnet_id' in interface_info: + msg = _("Cannot specify both subnet-id and port-id") + raise exc.BadRequest(resource='router', msg=msg) + + res_dict = self._encode_resource(resource_id=router_id, + resource=interface_info) + status_code, res_info = self._request_backend(context, res_dict, + 'router', 'ADDINTERFACE') + if status_code != requests.codes.ok: + self._raise_contrail_error(status_code, info=res_info, + obj_name='add_router_interface') + return res_info + + def remove_router_interface(self, context, router_id, interface_info): + """Delete interface from a router.""" + + self._validate_router_interface_request(interface_info) + + res_dict = self._encode_resource(resource_id=router_id, + resource=interface_info) + status_code, res_info = self._request_backend(context, res_dict, + 'router', 'DELINTERFACE') + if status_code != requests.codes.ok: + self._raise_contrail_error(status_code, info=res_info, + obj_name='remove_router_interface') + return res_info + + # Floating IP API handlers + def create_floatingip(self, context, floatingip): + """Creates a floating IP.""" + + return self._create_resource('floatingip', context, floatingip) + + def update_floatingip(self, context, fip_id, floatingip): + """Updates the attributes of a floating IP.""" + + return self._update_resource('floatingip', context, fip_id, + floatingip) + + def get_floatingip(self, context, fip_id, fields=None): + """Get the attributes of a floating ip.""" + + return self._get_resource('floatingip', context, fip_id, fields) + + def delete_floatingip(self, context, fip_id): + """Deletes a floating IP.""" + + self._delete_resource('floatingip', context, fip_id) + + def get_floatingips(self, context, filters=None, fields=None): + """Retrieves all floating ips identifiers.""" + + return self._list_resource('floatingip', context, filters, fields) + + def get_floatingips_count(self, context, filters=None): + """Get the count of floating IPs.""" + + fips_count = self._count_resource('floatingip', context, filters) + return fips_count['count'] + + # Security Group handlers + def create_security_group(self, context, security_group): + """Creates a Security Group.""" + + return self._create_resource('security_group', context, + security_group) + + def get_security_group(self, context, sg_id, fields=None, tenant_id=None): + """Get the attributes of a security group.""" + + return self._get_resource('security_group', context, sg_id, fields) + + def update_security_group(self, context, sg_id, security_group): + """Updates the attributes of a security group.""" + + return self._update_resource('security_group', context, sg_id, + security_group) + + def delete_security_group(self, context, sg_id): + """Deletes a security group.""" + + self._delete_resource('security_group', context, sg_id) + + def get_security_groups(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + """Retrieves all security group identifiers.""" + + return self._list_resource('security_group', context, + filters, fields) + + def create_security_group_rule(self, context, security_group_rule): + """Creates a security group rule.""" + + return self._create_resource('security_group_rule', context, + security_group_rule) + + def delete_security_group_rule(self, context, sg_rule_id): + """Deletes a security group rule.""" + + self._delete_resource('security_group_rule', context, sg_rule_id) + + def get_security_group_rule(self, context, sg_rule_id, fields=None): + """Get the attributes of a security group rule.""" + + return self._get_resource('security_group_rule', context, + sg_rule_id, fields) + + def get_security_group_rules(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + """Retrieves all security group rules.""" + + return self._list_resource('security_group_rule', context, + filters, fields) diff --git a/neutron/tests/unit/opencontrail/__init__.py b/neutron/tests/unit/opencontrail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/tests/unit/opencontrail/test_contrail_plugin.py b/neutron/tests/unit/opencontrail/test_contrail_plugin.py new file mode 100644 index 000000000..a98339201 --- /dev/null +++ b/neutron/tests/unit/opencontrail/test_contrail_plugin.py @@ -0,0 +1,315 @@ +# Copyright 2014 Juniper Networks. 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 datetime +import uuid + +import mock +import netaddr +from oslo.config import cfg +from testtools import matchers +import webob.exc + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import base as api_base +from neutron.common import exceptions as exc +from neutron import context as neutron_context +from neutron.db import api as db +from neutron.db import db_base_plugin_v2 +from neutron.db import external_net_db +from neutron.db import l3_db +from neutron.db import quota_db # noqa +from neutron.db import securitygroups_db +from neutron.extensions import portbindings +from neutron.extensions import securitygroup as ext_sg +from neutron.openstack.common import jsonutils +from neutron.tests.unit import _test_extension_portbindings as test_bindings +from neutron.tests.unit import test_db_plugin as test_plugin +from neutron.tests.unit import test_extension_security_group as test_sg +from neutron.tests.unit import test_extensions +from neutron.tests.unit import test_l3_plugin + + +CONTRAIL_PKG_PATH = "neutron.plugins.opencontrail.contrail_plugin" + + +class FakeServer(db_base_plugin_v2.NeutronDbPluginV2, + external_net_db.External_net_db_mixin, + securitygroups_db.SecurityGroupDbMixin, + l3_db.L3_NAT_db_mixin): + """FakeServer for contrail api server. + + This class mocks behaviour of contrail API server. + """ + supported_extension_aliases = ['external-net', 'router', 'floatingip'] + + @property + def _core_plugin(self): + return self + + def create_port(self, context, port): + self._ensure_default_security_group_on_port(context, port) + sgids = self._get_security_groups_on_port(context, port) + result = super(FakeServer, self).create_port(context, port) + self._process_port_create_security_group(context, result, sgids) + return result + + def update_port(self, context, id, port): + original_port = self.get_port(context, id) + updated_port = super(FakeServer, self).update_port(context, id, port) + port_updates = port['port'] + if ext_sg.SECURITYGROUPS in port_updates: + port_updates[ext_sg.SECURITYGROUPS] = ( + self._get_security_groups_on_port(context, port)) + self._delete_port_security_group_bindings(context, id) + self._process_port_create_security_group( + context, + updated_port, + port_updates[ext_sg.SECURITYGROUPS]) + else: + updated_port[ext_sg.SECURITYGROUPS] = ( + original_port[ext_sg.SECURITYGROUPS]) + + return updated_port + + def delete_port(self, context, id, l3_port_check=True): + if l3_port_check: + self.prevent_l3_port_deletion(context, id) + self.disassociate_floatingips(context, id) + super(FakeServer, self).delete_port(context, id) + + def create_subnet(self, context, subnet): + subnet_data = subnet['subnet'] + if subnet_data['gateway_ip'] == '0.0.0.0': + subnet_data['gateway_ip'] = None + return super(FakeServer, self).create_subnet(context, subnet) + + def create_network(self, context, network): + net_data = network['network'] + tenant_id = self._get_tenant_id_for_create(context, net_data) + self._ensure_default_security_group(context, tenant_id) + result = super(FakeServer, self).create_network(context, network) + self._process_l3_create(context, result, network['network']) + return result + + def update_network(self, context, id, network): + with context.session.begin(subtransactions=True): + result = super( + FakeServer, self).update_network(context, id, network) + self._process_l3_update(context, result, network['network']) + return result + + def delete_network(self, context, id): + self.delete_disassociated_floatingips(context, id) + super(FakeServer, self).delete_network(context, id) + + def request(self, *args, **kwargs): + request_data = jsonutils.loads(kwargs['data']) + context_dict = request_data['context'] + context = neutron_context.Context.from_dict(context_dict) + resource_type = context_dict['type'] + operation = context_dict['operation'] + data = request_data['data'] + resource = None + if data.get('resource'): + body = data['resource'] + if resource_type not in [ + 'security_group_rule', 'router', 'floatingip']: + for key, value in body.items(): + if value is None: + body[key] = attr.ATTR_NOT_SPECIFIED + resource = {resource_type: body} + + obj = {} + code = webob.exc.HTTPOk.code + try: + if operation == 'READ': + func = getattr(self, 'get_%s' % resource_type) + obj = func(context, data['id']) + if operation == 'READALL': + func = getattr(self, 'get_%ss' % resource_type) + obj = func(context, filters=data.get('filters')) + if operation == 'READCOUNT': + func = getattr(self, 'get_%ss_count' % resource_type) + count = func(context, filters=data.get('filters')) + obj = {'count': count} + if operation == 'CREATE': + func = getattr(self, 'create_%s' % resource_type) + obj = func(context, resource) + if operation == 'UPDATE': + func = getattr(self, 'update_%s' % resource_type) + obj = func(context, data['id'], resource) + if operation == 'DELETE': + func = getattr(self, 'delete_%s' % resource_type) + obj = func(context, data['id']) + if operation == 'ADDINTERFACE': + obj = self.add_router_interface( + context, data['id'], data['resource']) + if operation == 'DELINTERFACE': + obj = self.remove_router_interface( + context, data['id'], data['resource']) + except (exc.NeutronException, + netaddr.AddrFormatError) as error: + for fault in api_base.FAULT_MAP: + if isinstance(error, fault): + mapped_exc = api_base.FAULT_MAP[fault] + code = mapped_exc.code + obj = {'type': error.__class__.__name__, + 'message': error.msg, 'detail': ''} + if data.get('id'): + obj['id'] = data.get('id') + response = mock.MagicMock() + response.status_code = code + + def return_obj(): + return obj + response.json = return_obj + return response + + +FAKE_SERVER = FakeServer() + + +class Context(object): + def __init__(self, tenant_id=''): + self.read_only = False + self.show_deleted = False + self.roles = [u'admin', u'KeystoneServiceAdmin', u'KeystoneAdmin'] + self._read_deleted = 'no' + self.timestamp = datetime.datetime.now() + self.auth_token = None + self._session = None + self._is_admin = True + self.admin = uuid.uuid4().hex.decode() + self.request_id = 'req-' + str(uuid.uuid4()) + self.tenant = tenant_id + + +class KeyStoneInfo(object): + """To generate Keystone Authentication information + Contrail Driver expects Keystone auth info for testing purpose. + """ + auth_protocol = 'http' + auth_host = 'host' + auth_port = 5000 + admin_user = 'neutron' + admin_password = 'neutron' + admin_token = 'neutron' + admin_tenant_name = 'neutron' + + +class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase): + _plugin_name = ('%s.NeutronPluginContrailCoreV2' % CONTRAIL_PKG_PATH) + + def setUp(self, plugin=None, ext_mgr=None): + + cfg.CONF.keystone_authtoken = KeyStoneInfo() + mock.patch('requests.post').start().side_effect = FAKE_SERVER.request + db.configure_db() + super(ContrailPluginTestCase, self).setUp(self._plugin_name) + + +class TestContrailNetworksV2(test_plugin.TestNetworksV2, + ContrailPluginTestCase): + def setUp(self): + super(TestContrailNetworksV2, self).setUp() + + +class TestContrailSubnetsV2(test_plugin.TestSubnetsV2, + ContrailPluginTestCase): + def setUp(self): + super(TestContrailSubnetsV2, self).setUp() + + # Support ipv6 in contrail is planned in Juno + def test_update_subnet_ipv6_attributes(self): + self.skipTest("Contrail isn't supporting ipv6 yet") + + def test_update_subnet_ipv6_inconsistent_address_attribute(self): + self.skipTest("Contrail isn't supporting ipv6 yet") + + def test_update_subnet_ipv6_inconsistent_enable_dhcp(self): + self.skipTest("Contrail isn't supporting ipv6 yet") + + def test_update_subnet_ipv6_inconsistent_ra_attribute(self): + self.skipTest("Contrail isn't supporting ipv6 yet") + + def test_delete_subnet_dhcp_port_associated_with_other_subnets(self): + self.skipTest("There is no dhcp port in contrail") + + def _helper_test_validate_subnet(self, option, exception): + cfg.CONF.set_override(option, 0) + with self.network() as network: + subnet = {'network_id': network['network']['id'], + 'cidr': '10.0.2.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id'], + 'gateway_ip': '10.0.2.1', + 'dns_nameservers': ['8.8.8.8'], + 'host_routes': [{'destination': '135.207.0.0/16', + 'nexthop': '1.2.3.4'}]} + error = self.assertRaises(exception, + FAKE_SERVER._validate_subnet, + neutron_context.get_admin_context( + load_admin_roles=False), + subnet) + self.assertThat( + str(error), + matchers.Not(matchers.Contains('built-in function id'))) + + +class TestContrailPortsV2(test_plugin.TestPortsV2, + ContrailPluginTestCase): + def setUp(self): + super(TestContrailPortsV2, self).setUp() + + def test_delete_ports_by_device_id(self): + self.skipTest("This method tests rpc API of " + "which contrail isn't using") + + def test_delete_ports_by_device_id_second_call_failure(self): + self.skipTest("This method tests rpc API of " + "which contrail isn't using") + + def test_delete_ports_ignores_port_not_found(self): + self.skipTest("This method tests private method of " + "which contrail isn't using") + + +class TestContrailSecurityGroups(test_sg.TestSecurityGroups, + ContrailPluginTestCase): + def setUp(self, plugin=None, ext_mgr=None): + super(TestContrailSecurityGroups, self).setUp(self._plugin_name, + ext_mgr) + ext_mgr = extensions.PluginAwareExtensionManager.get_instance() + self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) + + +class TestContrailPortBinding(ContrailPluginTestCase, + test_bindings.PortBindingsTestCase): + VIF_TYPE = portbindings.VIF_TYPE_VROUTER + HAS_PORT_FILTER = True + + def setUp(self): + super(TestContrailPortBinding, self).setUp() + + +class TestContrailL3NatTestCase(ContrailPluginTestCase, + test_l3_plugin.L3NatDBIntTestCase): + mock_rescheduling = False + + def setUp(self): + super(TestContrailL3NatTestCase, self).setUp() diff --git a/setup.cfg b/setup.cfg index 2aa87ec5e..36efdcdca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,7 @@ data_files = etc/neutron/plugins/plumgrid = etc/neutron/plugins/plumgrid/plumgrid.ini etc/neutron/plugins/ryu = etc/neutron/plugins/ryu/ryu.ini etc/neutron/plugins/vmware = etc/neutron/plugins/vmware/nsx.ini + etc/neutron/plugins/opencontrail = etc/neutron/plugins/opencontrail/contrailplugin.ini scripts = bin/neutron-rootwrap bin/neutron-rootwrap-xen-dom0