From 18b564a180c7ea44e0f4ae2c775b242e50437139 Mon Sep 17 00:00:00 2001 From: Claudiu Belu Date: Tue, 17 Feb 2015 03:35:32 +0200 Subject: [PATCH] Adds Hyper-V NVGRE support Hyper-V Network Virtualization (HNV) was first introduced in Windows Hyper-V / Server 2012 and has the purpose of enabling the virtualization of Layer 2 and Layer 3 networking models. One of the HNV configuration approches is called NVGRE (Network Virtualization through GRE). Adds NVGRE related Utils and Ops class. Adds check in HyperVMechanismDriver if the given agent has NVGRE in it's reported configuration, in order to properly bind NVGRE neutron ports. Adds neutron_client implementation to fetch necessary information for NVGRE CustomerRoutes and LookupRecords. Emits ``lookup_update`` notifications when a new LookupRecord is updated. Registers HyperVNeutronAgent to ``lookup_update`` notifications and updates the given LookupRecord locally. Adds handle for ``tunnel_update`` notifications. Emits ``tunnel_update`` notification when HyperVNeutronAgent starts, in order for OpenVSwitch agents to create their own tunnels towards the agent. Implements: blueprint hyper-v-nvgre Change-Id: I8cf07770ae567ad3a1f3c906417e94133b00958c --- hyperv/neutron/constants.py | 8 + hyperv/neutron/hyperv_agent_notifier.py | 66 +++++ hyperv/neutron/hyperv_neutron_agent.py | 93 ++++++ hyperv/neutron/ml2/mech_hyperv.py | 8 +- hyperv/neutron/neutron_client.py | 134 +++++++++ hyperv/neutron/nvgre_ops.py | 176 +++++++++++ hyperv/neutron/utils.py | 4 + hyperv/neutron/utils_nvgre.py | 219 ++++++++++++++ hyperv/neutron/utilsv2.py | 48 +++ .../neutron/test_hyperv_agent_notifier.py | 65 ++++ .../unit/neutron/test_hyperv_neutron_agent.py | 109 +++++++ hyperv/tests/unit/neutron/test_mech_hyperv.py | 45 +++ .../tests/unit/neutron/test_neutron_client.py | 133 +++++++++ hyperv/tests/unit/neutron/test_nvgre_ops.py | 183 ++++++++++++ hyperv/tests/unit/neutron/test_utils.py | 9 + hyperv/tests/unit/neutron/test_utils_nvgre.py | 278 ++++++++++++++++++ hyperv/tests/unit/neutron/test_utilsv2.py | 53 ++++ requirements.txt | 2 + 18 files changed, 1632 insertions(+), 1 deletion(-) create mode 100644 hyperv/neutron/hyperv_agent_notifier.py create mode 100644 hyperv/neutron/neutron_client.py create mode 100644 hyperv/neutron/nvgre_ops.py create mode 100644 hyperv/neutron/utils_nvgre.py create mode 100644 hyperv/tests/unit/neutron/test_hyperv_agent_notifier.py create mode 100644 hyperv/tests/unit/neutron/test_mech_hyperv.py create mode 100644 hyperv/tests/unit/neutron/test_neutron_client.py create mode 100644 hyperv/tests/unit/neutron/test_nvgre_ops.py create mode 100644 hyperv/tests/unit/neutron/test_utils_nvgre.py diff --git a/hyperv/neutron/constants.py b/hyperv/neutron/constants.py index 4ec8be9..4691588 100644 --- a/hyperv/neutron/constants.py +++ b/hyperv/neutron/constants.py @@ -14,7 +14,12 @@ # under the License. # Topic for tunnel notifications between the plugin and agent +AGENT_TOPIC = 'q-agent-notifier' + TUNNEL = 'tunnel' +LOOKUP = 'lookup' + +UPDATE = 'update' # Special vlan_id value in ovs_vlan_allocations table indicating flat network FLAT_VLAN_ID = -1 @@ -23,3 +28,6 @@ TRUNK_ENDPOINT_MODE = 5 TYPE_FLAT = 'flat' TYPE_LOCAL = 'local' TYPE_VLAN = 'vlan' +TYPE_NVGRE = 'gre' + +IPV4_DEFAULT = '0.0.0.0' diff --git a/hyperv/neutron/hyperv_agent_notifier.py b/hyperv/neutron/hyperv_agent_notifier.py new file mode 100644 index 0000000..8f57a28 --- /dev/null +++ b/hyperv/neutron/hyperv_agent_notifier.py @@ -0,0 +1,66 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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_log import log as logging + +from hyperv.neutron import constants + +LOG = logging.getLogger(__name__) + + +def get_topic_name(prefix, table, operation): + """Create a topic name. + + The topic name needs to be synced between the agents. + The agent will send a fanout message to all of the listening agents + so that the agents in turn can perform their updates accordingly. + + :param prefix: Common prefix for the agent message queues. + :param table: The table in question (TUNNEL, LOOKUP). + :param operation: The operation that invokes notification (UPDATE) + :returns: The topic name. + """ + return '%s-%s-%s' % (prefix, table, operation) + + +class AgentNotifierApi(object): + """Agent side of the OpenVSwitch rpc API.""" + + def __init__(self, topic, client): + self._client = client + self.topic_tunnel_update = get_topic_name(topic, + constants.TUNNEL, + constants.UPDATE) + self.topic_lookup_update = get_topic_name(topic, + constants.LOOKUP, + constants.UPDATE) + + def _fanout_cast(self, context, topic, method, **info): + cctxt = self._client.prepare(topic=topic, fanout=True) + cctxt.cast(context, method, **info) + + def tunnel_update(self, context, tunnel_ip, tunnel_type): + self._fanout_cast(context, + self.topic_tunnel_update, + 'tunnel_update', + tunnel_ip=tunnel_ip, + tunnel_type=tunnel_type) + + def lookup_update(self, context, lookup_ip, lookup_details): + self._fanout_cast(context, + self.topic_lookup_update, + 'lookup_update', + lookup_ip=lookup_ip, + lookup_details=lookup_details) diff --git a/hyperv/neutron/hyperv_neutron_agent.py b/hyperv/neutron/hyperv_neutron_agent.py index 8347e10..3e2ca0e 100644 --- a/hyperv/neutron/hyperv_neutron_agent.py +++ b/hyperv/neutron/hyperv_neutron_agent.py @@ -18,13 +18,34 @@ import collections import re import time +from oslo_config import cfg from oslo_log import log as logging from hyperv.common.i18n import _, _LE, _LI # noqa from hyperv.neutron import constants +from hyperv.neutron import hyperv_agent_notifier +from hyperv.neutron import nvgre_ops from hyperv.neutron import utils from hyperv.neutron import utilsfactory +nvgre_opts = [ + cfg.BoolOpt('enable_support', + default=False, + help=_('Enables Hyper-V NVGRE. ' + 'Requires Windows Server 2012 or above.')), + cfg.IntOpt('provider_vlan_id', + default=0, + help=_('Specifies the VLAN ID of the physical network, required' + ' for setting the NVGRE Provider Address.')), + cfg.StrOpt('provider_tunnel_ip', + default=None, + help=_('Specifies the tunnel IP which will be used and ' + 'reported by this host for NVGRE networks.')), +] + +CONF = cfg.CONF +CONF.register_opts(nvgre_opts, "NVGRE") + LOG = logging.getLogger(__name__) @@ -57,6 +78,13 @@ networking-plugin-hyperv_agent.html self._port_metric_retries = {} self.plugin_rpc = None + self.context = None + self.client = None + self.connection = None + self.endpoints = None + self.topic = constants.AGENT_TOPIC + + self._nvgre_enabled = False conf = conf or {} agent_conf = conf.get('AGENT', {}) @@ -89,8 +117,48 @@ networking-plugin-hyperv_agent.html vswitch = parts[1].strip() self._physical_network_mappings[pattern] = vswitch + def _init_nvgre(self): + # if NVGRE is enabled, self._nvgre_ops is required in order to properly + # set the agent state (see get_agent_configrations method). + + if not CONF.NVGRE.enable_support: + return + + if not CONF.NVGRE.provider_tunnel_ip: + err_msg = _('enable_nvgre_support is set to True, but provider ' + 'tunnel IP is not configured. Check neutron.conf ' + 'config file.') + LOG.error(err_msg) + raise utils.HyperVException(msg=err_msg) + + self._nvgre_enabled = True + self._nvgre_ops = nvgre_ops.HyperVNvgreOps( + self._physical_network_mappings.values()) + + self._nvgre_ops.init_notifier(self.context, self.client) + self._nvgre_ops.tunnel_update(self.context, + CONF.NVGRE.provider_tunnel_ip, + constants.TYPE_NVGRE) + + # setup Hyper-V Agent Lookup Record update consumer + topic = hyperv_agent_notifier.get_topic_name( + self.topic, constants.LOOKUP, constants.UPDATE) + self.connection.create_consumer(topic, self.endpoints, fanout=True) + + # the created consumer is the last connection server. + # need to start it in order for it to consume. + self.connection.servers[-1].start() + def get_agent_configurations(self): configurations = {'vswitch_mappings': self._physical_network_mappings} + if CONF.NVGRE.enable_support: + configurations['arp_responder_enabled'] = False + configurations['tunneling_ip'] = CONF.NVGRE.provider_tunnel_ip + configurations['devices'] = 1 + configurations['l2_population'] = False + configurations['tunnel_types'] = [constants.TYPE_NVGRE] + configurations['enable_distributed_routing'] = False + configurations['bridge_mappings'] = {} return configurations def _get_vswitch_for_physical_network(self, phys_network_name): @@ -135,6 +203,20 @@ networking-plugin-hyperv_agent.html network_type, physical_network, segmentation_id, port['admin_state_up']) + def tunnel_update(self, context, **kwargs): + LOG.info(_LI('tunnel_update received: kwargs: %s'), kwargs) + tunnel_ip = kwargs.get('tunnel_ip') + if tunnel_ip == CONF.NVGRE.provider_tunnel_ip: + # the notification should be ignored if it originates from this + # node. + return + + tunnel_type = kwargs.get('tunnel_type') + self._nvgre_ops.tunnel_update(context, tunnel_ip, tunnel_type) + + def lookup_update(self, context, **kwargs): + self._nvgre_ops.lookup_update(kwargs) + def _get_vswitch_name(self, network_type, physical_network): if network_type != constants.TYPE_LOCAL: vswitch_name = self._get_vswitch_for_physical_network( @@ -153,6 +235,9 @@ networking-plugin-hyperv_agent.html if network_type == constants.TYPE_VLAN: self._utils.set_switch_external_port_trunk_vlan( vswitch_name, segmentation_id, constants.TRUNK_ENDPOINT_MODE) + elif network_type == constants.TYPE_NVGRE and self._nvgre_enabled: + self._nvgre_ops.bind_nvgre_network( + segmentation_id, net_uuid, vswitch_name) elif network_type == constants.TYPE_FLAT: # Nothing to do pass @@ -201,6 +286,9 @@ networking-plugin-hyperv_agent.html self._utils.set_vswitch_port_vlan_id( segmentation_id, port_id) + elif network_type == constants.TYPE_NVGRE and self._nvgre_enabled: + self._nvgre_ops.bind_nvgre_port( + segmentation_id, map['vswitch_name'], port_id) elif network_type == constants.TYPE_FLAT: # Nothing to do pass @@ -336,6 +424,9 @@ networking-plugin-hyperv_agent.html return (resync_a | resync_b) def daemon_loop(self): + # init NVGRE after the RPC connection and context is created. + self._init_nvgre() + sync = True ports = set() @@ -356,6 +447,8 @@ networking-plugin-hyperv_agent.html sync = self._process_network_ports(port_info) ports = port_info['current'] + if self._nvgre_enabled: + self._nvgre_ops.refresh_nvgre_records() self._port_enable_control_metrics() except Exception: LOG.exception(_LE("Error in agent event loop")) diff --git a/hyperv/neutron/ml2/mech_hyperv.py b/hyperv/neutron/ml2/mech_hyperv.py index 30addb8..4a97dd7 100644 --- a/hyperv/neutron/ml2/mech_hyperv.py +++ b/hyperv/neutron/ml2/mech_hyperv.py @@ -29,7 +29,13 @@ class HypervMechanismDriver(object): """ def get_allowed_network_types(self, agent=None): - return [constants.TYPE_LOCAL, constants.TYPE_FLAT, constants.TYPE_VLAN] + network_types = [constants.TYPE_LOCAL, constants.TYPE_FLAT, + constants.TYPE_VLAN] + if agent is not None: + tunnel_types = agent.get('configurations', {}).get('tunnel_types') + if tunnel_types: + network_types.extend(tunnel_types) + return network_types def get_mappings(self, agent): return agent['configurations'].get('vswitch_mappings', {}) diff --git a/hyperv/neutron/neutron_client.py b/hyperv/neutron/neutron_client.py new file mode 100644 index 0000000..795e3f8 --- /dev/null +++ b/hyperv/neutron/neutron_client.py @@ -0,0 +1,134 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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 neutronclient.v2_0 import client as clientv20 +from oslo_config import cfg +from oslo_log import log as logging + +from hyperv.common.i18n import _LW, _LE # noqa +from hyperv.neutron import constants + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +neutron_opts = [ + cfg.StrOpt('url', + default='http://127.0.0.1:9696', + help='URL for connecting to neutron'), + cfg.IntOpt('url_timeout', + default=30, + help='timeout value for connecting to neutron in seconds'), + cfg.StrOpt('admin_username', + help='username for connecting to neutron in admin context'), + cfg.StrOpt('admin_password', + help='password for connecting to neutron in admin context', + secret=True), + cfg.StrOpt('admin_tenant_name', + help='tenant name for connecting to neutron in admin context'), + cfg.StrOpt('admin_auth_url', + default='http://localhost:5000/v2.0', + help='auth url for connecting to neutron in admin context'), + cfg.StrOpt('auth_strategy', + default='keystone', + help='auth strategy for connecting to neutron in admin context') +] + +CONF.register_opts(neutron_opts, 'neutron') + + +class NeutronAPIClient(object): + + def __init__(self): + self._init_client() + + def _init_client(self, token=None): + params = { + 'endpoint_url': CONF.neutron.url, + 'timeout': CONF.neutron.url_timeout, + 'insecure': True, + 'ca_cert': None, + } + + if token: + params['token'] = token + params['auth_strategy'] = None + else: + params['username'] = CONF.neutron.admin_username + params['tenant_name'] = CONF.neutron.admin_tenant_name + params['password'] = CONF.neutron.admin_password + params['auth_url'] = CONF.neutron.admin_auth_url + params['auth_strategy'] = CONF.neutron.auth_strategy + + self._client = clientv20.Client(**params) + + def get_network_subnets(self, network_id): + try: + net = self._client.show_network(network_id) + return net['network']['subnets'] + except Exception as ex: + LOG.error(_LE("Could not retrieve network %(network_id)s . Error: " + "%(ex)s"), {'network_id': network_id, 'ex': ex}) + + return [] + + def get_network_subnet_cidr_and_gateway(self, subnet_id): + try: + subnet = self._client.show_subnet(subnet_id)['subnet'] + return (str(subnet['cidr']), str(subnet['gateway_ip'])) + except Exception as ex: + LOG.error(_LE("Could not retrieve subnet %(subnet_id)s . Error: " + "%(ex)s: "), {'subnet_id': subnet_id, 'ex': ex}) + + return None, None + + def get_port_ip_address(self, port_id): + try: + port = self._client.show_port(port_id) + fixed_ips = port['port']['fixed_ips'][0] + return fixed_ips['ip_address'] + except Exception as ex: + LOG.error(_LE("Could not retrieve port %(port_id)s . Error: " + "%(ex)s"), {'port_id': port_id, 'ex': ex}) + + return None + + def get_tunneling_agents(self): + try: + agents = self._client.list_agents() + tunneling_agents = [ + a for a in agents['agents'] if constants.TYPE_NVGRE in + a.get('configurations', {}).get('tunnel_types', [])] + + tunneling_ip_agents = [ + a for a in tunneling_agents if + a.get('configurations', {}).get('tunneling_ip')] + + if len(tunneling_ip_agents) < len(tunneling_agents): + LOG.warning(_LW('Some agents have NVGRE tunneling enabled, but' + ' do not provide tunneling_ip. Ignoring those ' + 'agents.')) + + return dict([(a['host'], a['configurations']['tunneling_ip']) + for a in tunneling_ip_agents]) + except Exception as ex: + LOG.error(_LE("Could not get tunneling agents. Error: %s"), ex) + return {} + + def get_network_ports(self, **kwargs): + try: + return self._client.list_ports(**kwargs)['ports'] + except Exception as ex: + LOG.error(_LE("Exception caught: %s"), ex) + return [] diff --git a/hyperv/neutron/nvgre_ops.py b/hyperv/neutron/nvgre_ops.py new file mode 100644 index 0000000..8cb8d14 --- /dev/null +++ b/hyperv/neutron/nvgre_ops.py @@ -0,0 +1,176 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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 +from oslo_log import log as logging + +from hyperv.common.i18n import _LI, _LW, _LE # noqa +from hyperv.neutron import constants +from hyperv.neutron import hyperv_agent_notifier +from hyperv.neutron import neutron_client +from hyperv.neutron import utils_nvgre +from hyperv.neutron import utilsfactory + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + + +class HyperVNvgreOps(object): + + def __init__(self, physical_networks): + self.topic = constants.AGENT_TOPIC + self._vswitch_ips = {} + self._tunneling_agents = {} + self._nvgre_ports = [] + self._network_vsids = {} + + self._hyperv_utils = utilsfactory.get_hypervutils() + self._nvgre_utils = utils_nvgre.NvgreUtils() + self._n_client = neutron_client.NeutronAPIClient() + + self._init_nvgre(physical_networks) + + def init_notifier(self, context, rpc_client): + self.context = context + self._notifier = hyperv_agent_notifier.AgentNotifierApi( + self.topic, rpc_client) + + def _init_nvgre(self, physical_networks): + for network in physical_networks: + LOG.info(_LI("Adding provider route and address for network: %s"), + network) + self._nvgre_utils.create_provider_route(network) + self._nvgre_utils.create_provider_address( + network, CONF.NVGRE.provider_vlan_id) + ip_addr, length = self._nvgre_utils.get_network_iface_ip(network) + self._vswitch_ips[network] = ip_addr + + def _refresh_tunneling_agents(self): + self._tunneling_agents.update(self._n_client.get_tunneling_agents()) + + def lookup_update(self, kwargs): + lookup_ip = kwargs.get('lookup_ip') + lookup_details = kwargs.get('lookup_details') + + LOG.info(_LI("Lookup Received: %(lookup_ip)s, %(lookup_details)s"), + {'lookup_ip': lookup_ip, 'lookup_details': lookup_details}) + if not lookup_ip or not lookup_details: + return + + self._register_lookup_record(lookup_ip, + lookup_details['customer_addr'], + lookup_details['mac_addr'], + lookup_details['customer_vsid']) + + def tunnel_update(self, context, tunnel_ip, tunnel_type): + if tunnel_type != constants.TYPE_NVGRE: + return + self._notifier.tunnel_update(context, CONF.NVGRE.provider_tunnel_ip, + tunnel_type) + + def _register_lookup_record(self, prov_addr, cust_addr, mac_addr, vsid): + LOG.info(_LI('Creating LookupRecord: VSID: %(vsid)s MAC: %(mac_addr)s ' + 'Customer IP: %(cust_addr)s Provider IP: %(prov_addr)s'), + dict(vsid=vsid, + mac_addr=mac_addr, + cust_addr=cust_addr, + prov_addr=prov_addr)) + + self._nvgre_utils.create_lookup_record( + prov_addr, cust_addr, mac_addr, vsid) + + def bind_nvgre_port(self, segmentation_id, network_name, port_id): + mac_addr = self._hyperv_utils.get_vnic_mac_address(port_id) + provider_addr = self._nvgre_utils.get_network_iface_ip(network_name)[0] + customer_addr = self._n_client.get_port_ip_address(port_id) + + if not provider_addr or not customer_addr: + LOG.warning(_LW('Cannot bind NVGRE port. Could not determine ' + 'provider address (%(prov_addr)s) or customer ' + 'address (%(cust_addr)s).'), + {'prov_addr': provider_addr, + 'cust_addr': customer_addr}) + return + + LOG.info(_LI('Binding VirtualSubnetID %(segmentation_id)s ' + 'to switch port %(port_id)s'), + dict(segmentation_id=segmentation_id, port_id=port_id)) + self._hyperv_utils.set_vswitch_port_vsid(segmentation_id, port_id) + + # normal lookup record. + self._register_lookup_record( + provider_addr, customer_addr, mac_addr, segmentation_id) + + # lookup record for dhcp requests. + self._register_lookup_record( + self._vswitch_ips[network_name], constants.IPV4_DEFAULT, + mac_addr, segmentation_id) + + LOG.info('Fanning out LookupRecord...') + self._notifier.lookup_update(self.context, + provider_addr, + {'customer_addr': customer_addr, + 'mac_addr': mac_addr, + 'customer_vsid': segmentation_id}) + + def bind_nvgre_network(self, segmentation_id, net_uuid, vswitch_name): + subnets = self._n_client.get_network_subnets(net_uuid) + if len(subnets) > 1: + LOG.warning(_LW("Multiple subnets in the same network is not " + "supported.")) + subnet = subnets[0] + try: + cidr, gw = self._n_client.get_network_subnet_cidr_and_gateway( + subnet) + self._nvgre_utils.create_customer_routes( + segmentation_id, vswitch_name, cidr, gw) + except Exception as ex: + LOG.error(_LE("Exception caught: %s"), ex) + + self._network_vsids[net_uuid] = segmentation_id + self.refresh_nvgre_records(network_id=net_uuid) + self._notifier.tunnel_update( + self.context, CONF.NVGRE.provider_tunnel_ip, segmentation_id) + + def refresh_nvgre_records(self, **kwargs): + self._refresh_tunneling_agents() + ports = self._n_client.get_network_ports(**kwargs) + + # process ports that were not processed yet. + # process ports that are bound to tunneling_agents. + ports = [p for p in ports if p['id'] not in self._nvgre_ports and + p['binding:host_id'] in self._tunneling_agents and + p['network_id'] in self._network_vsids.keys()] + + for port in ports: + tunneling_ip = self._tunneling_agents[port['binding:host_id']] + customer_addr = port['fixed_ips'][0]['ip_address'] + mac_addr = port['mac_address'].replace(':', '') + segmentation_id = self._network_vsids[port['network_id']] + try: + self._register_lookup_record( + tunneling_ip, customer_addr, mac_addr, segmentation_id) + + self._nvgre_ports.append(port['id']) + except Exception as ex: + LOG.error(_LE("Exception while adding lookup_record: %(ex)s. " + "VSID: %(vsid)s MAC: %(mac_address)s Customer " + "IP:%(cust_addr)s Provider IP: %(prov_addr)s"), + dict(ex=ex, + vsid=segmentation_id, + mac_address=mac_addr, + cust_addr=customer_addr, + prov_addr=tunneling_ip)) diff --git a/hyperv/neutron/utils.py b/hyperv/neutron/utils.py index 0f23004..e3fffd9 100644 --- a/hyperv/neutron/utils.py +++ b/hyperv/neutron/utils.py @@ -95,6 +95,10 @@ class HyperVUtils(object): for p in self._conn.Msvm_SyntheticEthernetPortSettingData() if p.ElementName is not None) + def get_vnic_mac_address(self, switch_port_name): + vnic = self._get_vnic_settings(switch_port_name) + return vnic.Address + def _get_vnic_settings(self, vnic_name): vnic_settings = self._conn.Msvm_SyntheticEthernetPortSettingData( ElementName=vnic_name) diff --git a/hyperv/neutron/utils_nvgre.py b/hyperv/neutron/utils_nvgre.py new file mode 100644 index 0000000..56968c2 --- /dev/null +++ b/hyperv/neutron/utils_nvgre.py @@ -0,0 +1,219 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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 sys +import uuid + +from oslo_config import cfg +from oslo_log import log as logging + +from hyperv.common.i18n import _, _LI, _LW, _LE # noqa +from hyperv.neutron import constants +from hyperv.neutron import utilsv2 + +# Check needed for unit testing on Unix +if sys.platform == 'win32': + import wmi + +agent_opts = [ + cfg.StrOpt('neutron_metadata_address', + default='169.254.169.254', + help=_('Specifies the address which will serve the metadata for' + ' the instance.')), +] + +CONF = cfg.CONF +CONF.register_opts(agent_opts, "AGENT") + +LOG = logging.getLogger(__name__) + + +class NvgreUtils(object): + _HYPERV_VIRT_ADAPTER = 'Hyper-V Virtual Ethernet Adapter' + _IPV4_ADDRESS_FAMILY = 2 + + _WNV_BIND_NAME = 'Wnv' + _TRANSLATE_NAT = 0 + _TRANSLATE_ENCAP = 1 + + _LOOKUP_RECORD_TYPE_STATIC = 0 + _LOOKUP_RECORD_TYPE_L2_ONLY = 3 + + _STDCIMV2_NAMESPACE = '//./root/StandardCimv2' + + def __init__(self): + super(NvgreUtils, self).__init__() + self._utils = utilsv2.HyperVUtilsV2() + self._net_if_indexes = {} + if sys.platform == 'win32': + self._scimv2 = wmi.WMI(moniker=self._STDCIMV2_NAMESPACE) + + def create_provider_address(self, network_name, provider_vlan_id): + iface_index = self._get_network_iface_index(network_name) + (provider_addr, prefix_len) = self.get_network_iface_ip(network_name) + + if iface_index is None or provider_addr is None: + return None + + provider = ( + self._scimv2.MSFT_NetVirtualizationProviderAddressSettingData( + ProviderAddress=provider_addr)) + + if provider: + if (provider[0].VlanID == provider_vlan_id and + provider[0].InterfaceIndex == iface_index): + # ProviderAddress already exists. + return + # ProviderAddress exists, but with different VlanID or iface index. + provider[0].Delete_() + + self._create_new_object( + self._scimv2.MSFT_NetVirtualizationProviderAddressSettingData, + ProviderAddress=provider_addr, + VlanID=provider_vlan_id, + InterfaceIndex=iface_index, + PrefixLength=prefix_len) + + def create_provider_route(self, network_name): + iface_index = self._get_network_iface_index(network_name) + + if iface_index is None: + return + + routes = self._scimv2.MSFT_NetVirtualizationProviderRouteSettingData( + InterfaceIndex=iface_index, NextHop=constants.IPV4_DEFAULT) + + if not routes: + self._create_new_object( + self._scimv2.MSFT_NetVirtualizationProviderRouteSettingData, + InterfaceIndex=iface_index, + DestinationPrefix='%s/0' % constants.IPV4_DEFAULT, + NextHop=constants.IPV4_DEFAULT) + + def create_customer_routes(self, vsid, network_name, dest_prefix, gateway): + cust_route_string = network_name + dest_prefix + str(vsid) + rdid_uuid = uuid.uuid5(uuid.NAMESPACE_X500, cust_route_string) + rdid_uuid = str(rdid_uuid) + + routes = self._scimv2.MSFT_NetVirtualizationCustomerRouteSettingData( + VirtualSubnetID=vsid) + + for route in routes: + route.Delete_() + + # TODO(claudiub): the logic should be moved to nvgre_ops. This should + # create only one router per call. + + self._create_cust_route( + vsid, dest_prefix, constants.IPV4_DEFAULT, rdid_uuid) + + if not gateway: + LOG.info(_LI('Subnet does not have gateway configured. Skipping.')) + return + + if gateway.split('.')[-1] == '1': + LOG.error(_LE('Subnet has unsupported gateway IP ending in 1: %s. ' + 'Any other gateway IP is supported.'), gateway) + return + + # TODO(claudiub): what if there is no gateway? + self._create_cust_route( + vsid, '%s/0' % constants.IPV4_DEFAULT, gateway, rdid_uuid) + + # customer route for metadata is also necessary. + self._create_cust_route( + vsid, '%s/32' % CONF.AGENT.neutron_metadata_address, gateway, + rdid_uuid) + + def _create_cust_route(self, vsid, dest_prefix, next_hop, rdid_uuid): + self._create_new_object( + self._scimv2.MSFT_NetVirtualizationCustomerRouteSettingData, + VirtualSubnetID=vsid, + DestinationPrefix=dest_prefix, + NextHop=next_hop, + Metric=255, + RoutingDomainID='{%s}' % rdid_uuid) + + def create_lookup_record(self, provider_addr, customer_addr, mac, vsid): + # check for existing entry. + lrec = self._scimv2.MSFT_NetVirtualizationLookupRecordSettingData( + CustomerAddress=customer_addr, VirtualSubnetID=vsid) + if (lrec and lrec[0].VirtualSubnetID == vsid and + lrec[0].ProviderAddress == provider_addr and + lrec[0].MACAddress == mac): + # lookup record already exists, nothing to do. + return + + # create new lookup record. + if lrec: + lrec[0].Delete_() + + if constants.IPV4_DEFAULT == customer_addr: + # customer address used for DHCP requests. + record_type = self._LOOKUP_RECORD_TYPE_L2_ONLY + else: + record_type = self._LOOKUP_RECORD_TYPE_STATIC + + self._create_new_object( + self._scimv2.MSFT_NetVirtualizationLookupRecordSettingData, + VirtualSubnetID=vsid, + Rule=self._TRANSLATE_ENCAP, + Type=record_type, + MACAddress=mac, + CustomerAddress=customer_addr, + ProviderAddress=provider_addr) + + def _create_new_object(self, object_class, **args): + new_obj = object_class.new(**args) + new_obj.Put_() + return new_obj + + def _get_network_ifaces_by_name(self, network_name): + return [n for n in self._scimv2.MSFT_NetAdapter() if + n.Name.find(network_name) >= 0] + + def _get_network_iface_index(self, network_name): + if self._net_if_indexes.get(network_name): + return self._net_if_indexes[network_name] + + description = ( + self._utils.get_vswitch_external_network_name(network_name)) + + # physical NIC and vswitch must have the same MAC address. + networks = self._scimv2.MSFT_NetAdapter( + InterfaceDescription=description) + + if networks: + self._net_if_indexes[network_name] = networks[0].InterfaceIndex + return networks[0].InterfaceIndex + + def get_network_iface_ip(self, network_name): + networks = [n for n in self._get_network_ifaces_by_name(network_name) + if n.DriverDescription == self._HYPERV_VIRT_ADAPTER] + + if networks: + ip_addr = self._scimv2.MSFT_NetIPAddress( + InterfaceIndex=networks[0].InterfaceIndex, + AddressFamily=self._IPV4_ADDRESS_FAMILY) + + if ip_addr: + return (ip_addr[0].IPAddress, ip_addr[0].PrefixLength) + else: + LOG.error(_LE('No IP Address could be found for network: %s'), + network_name) + else: + LOG.error(_LE('No vswitch was found with name: %s'), network_name) + + return (None, None) diff --git a/hyperv/neutron/utilsv2.py b/hyperv/neutron/utilsv2.py index e72b458..997a790 100644 --- a/hyperv/neutron/utilsv2.py +++ b/hyperv/neutron/utilsv2.py @@ -145,10 +145,28 @@ class HyperVUtilsV2(utils.HyperVUtils): vswitch_name) return vswitch[0] + def _get_vswitch_external_port(self, vswitch_name): + vswitch = self._get_vswitch(vswitch_name) + ext_ports = self._conn.Msvm_ExternalEthernetPort() + for ext_port in ext_ports: + lan_endpoint_list = ext_port.associators( + wmi_result_class=self._LAN_ENDPOINT) + if lan_endpoint_list: + lan_endpoint_list = lan_endpoint_list[0].associators( + wmi_result_class=self._LAN_ENDPOINT) + if (lan_endpoint_list and + lan_endpoint_list[0].SystemName == vswitch.Name): + return ext_port + def set_switch_external_port_trunk_vlan(self, vswitch_name, vlan_id, desired_endpoint_mode): pass + def get_vswitch_external_network_name(self, vswitch_name): + ext_port = self._get_vswitch_external_port(vswitch_name) + if ext_port: + return ext_port.ElementName + def set_vswitch_port_vlan_id(self, vlan_id, switch_port_name): port_alloc, found = self._get_switch_port_allocation(switch_port_name) if not found: @@ -176,15 +194,45 @@ class HyperVUtilsV2(utils.HyperVUtils): port_alloc.path_(), [vlan_settings.GetText_(1)]) self._check_job_status(ret_val, job_path) + def set_vswitch_port_vsid(self, vsid, switch_port_name): + port_alloc, found = self._get_switch_port_allocation(switch_port_name) + if not found: + raise utils.HyperVException( + msg=_('Port Allocation not found: %s') % switch_port_name) + + vsid_settings = self._get_security_setting_data_from_port_alloc( + port_alloc) + + if vsid_settings: + if vsid_settings.VirtualSubnetId == vsid: + # VSID already added, no need to readd it. + return + # Removing the feature because it cannot be modified + # due to a wmi exception. + self._remove_virt_feature(vsid_settings) + + (vsid_settings, found) = self._get_security_setting_data( + switch_port_name) + vsid_settings.VirtualSubnetId = vsid + self._add_virt_feature(port_alloc, vsid_settings) + def _get_vlan_setting_data_from_port_alloc(self, port_alloc): return self._get_first_item(port_alloc.associators( wmi_result_class=self._PORT_VLAN_SET_DATA)) + def _get_security_setting_data_from_port_alloc(self, port_alloc): + return self._get_first_item(port_alloc.associators( + wmi_result_class=self._PORT_SECURITY_SET_DATA)) + def _get_vlan_setting_data(self, switch_port_name, create=True): return self._get_setting_data( self._PORT_VLAN_SET_DATA, switch_port_name, create) + def _get_security_setting_data(self, switch_port_name, create=True): + return self._get_setting_data( + self._PORT_SECURITY_SET_DATA, switch_port_name, create) + def _get_switch_port_allocation(self, switch_port_name, create=False): return self._get_setting_data( self._PORT_ALLOC_SET_DATA, diff --git a/hyperv/tests/unit/neutron/test_hyperv_agent_notifier.py b/hyperv/tests/unit/neutron/test_hyperv_agent_notifier.py new file mode 100644 index 0000000..ad66349 --- /dev/null +++ b/hyperv/tests/unit/neutron/test_hyperv_agent_notifier.py @@ -0,0 +1,65 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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. + +""" +Unit Tests for Hyper-V Agent Notifier. +""" + +import mock + +from hyperv.neutron import constants +from hyperv.neutron import hyperv_agent_notifier +from hyperv.tests import base + + +class TestAgentNotifierApi(base.BaseTestCase): + + def setUp(self): + super(TestAgentNotifierApi, self).setUp() + + self.notifier = hyperv_agent_notifier.AgentNotifierApi( + topic=constants.AGENT_TOPIC, client=mock.MagicMock()) + + def test_tunnel_update(self): + expected_topic = hyperv_agent_notifier.get_topic_name( + constants.AGENT_TOPIC, constants.TUNNEL, constants.UPDATE) + + self.notifier.tunnel_update(mock.sentinel.context, + mock.sentinel.tunnel_ip, + constants.TYPE_NVGRE) + + self.notifier._client.prepare.assert_called_once_with( + topic=expected_topic, fanout=True) + prepared_client = self.notifier._client.prepare.return_value + prepared_client.cast.assert_called_once_with( + mock.sentinel.context, 'tunnel_update', + tunnel_ip=mock.sentinel.tunnel_ip, + tunnel_type=constants.TYPE_NVGRE) + + def test_lookup_update(self): + expected_topic = hyperv_agent_notifier.get_topic_name( + constants.AGENT_TOPIC, constants.LOOKUP, constants.UPDATE) + + self.notifier.lookup_update(mock.sentinel.context, + mock.sentinel.lookup_ip, + mock.sentinel.lookup_details) + + self.notifier._client.prepare.assert_called_once_with( + topic=expected_topic, fanout=True) + prepared_client = self.notifier._client.prepare.return_value + prepared_client.cast.assert_called_once_with( + mock.sentinel.context, 'lookup_update', + lookup_ip=mock.sentinel.lookup_ip, + lookup_details=mock.sentinel.lookup_details) diff --git a/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py b/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py index f60f243..0fc2afb 100644 --- a/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py +++ b/hyperv/tests/unit/neutron/test_hyperv_neutron_agent.py @@ -21,6 +21,7 @@ Unit tests for Windows Hyper-V virtual switch neutron driver import mock from hyperv.neutron import constants +from hyperv.neutron import hyperv_agent_notifier from hyperv.neutron import hyperv_neutron_agent from hyperv.neutron import utils from hyperv.neutron import utilsfactory @@ -42,7 +43,12 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.agent._utils = mock.MagicMock() self.agent.sec_groups_agent = mock.MagicMock() self.agent.context = mock.Mock() + self.agent.client = mock.MagicMock() + self.agent.connection = mock.MagicMock() self.agent.agent_id = mock.Mock() + self.agent.notifier = mock.Mock() + self.agent._utils = mock.MagicMock() + self.agent._nvgre_ops = mock.MagicMock() def test_load_physical_network_mappings(self): test_mappings = ['fake_network_1:fake_vswitch', @@ -56,6 +62,54 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.assertEqual(expected, self.agent._physical_network_mappings.items()) + @mock.patch.object(hyperv_neutron_agent.nvgre_ops, 'HyperVNvgreOps') + def test_init_nvgre_disabled(self, mock_hyperv_nvgre_ops): + self.agent._init_nvgre() + self.assertFalse(mock_hyperv_nvgre_ops.called) + self.assertFalse(self.agent._nvgre_enabled) + + @mock.patch.object(hyperv_neutron_agent.nvgre_ops, 'HyperVNvgreOps') + def test_init_nvgre_no_tunnel_ip(self, mock_hyperv_nvgre_ops): + self.config(enable_support=True, group='NVGRE') + self.assertRaises(utils.HyperVException, self.agent._init_nvgre) + + @mock.patch.object(hyperv_neutron_agent.nvgre_ops, 'HyperVNvgreOps') + def test_init_nvgre_enabled(self, mock_hyperv_nvgre_ops): + self.config(enable_support=True, group='NVGRE') + self.config(provider_tunnel_ip=mock.sentinel.tunneling_ip, + group='NVGRE') + self.agent._init_nvgre() + mock_hyperv_nvgre_ops.assert_called_once_with( + self.agent._physical_network_mappings.values()) + + self.assertTrue(self.agent._nvgre_enabled) + self.agent._nvgre_ops.init_notifier.assert_called_once_with( + self.agent.context, self.agent.client) + expected_topic = hyperv_agent_notifier.get_topic_name( + self.agent.topic, constants.LOOKUP, constants.UPDATE) + self.agent.connection.create_consumer.assert_called_once_with( + expected_topic, self.agent.endpoints, fanout=True) + self.agent.connection.servers[-1].start.assert_called_once_with() + + def test_get_agent_configurations(self): + actual = self.agent.get_agent_configurations() + + self.assertEqual(self.agent._physical_network_mappings, + actual['vswitch_mappings']) + self.assertNotIn('tunnel_types', actual) + self.assertNotIn('tunneling_ip', actual) + + def test_get_agent_configurations_nvgre(self): + self.config(enable_support=True, group='NVGRE') + self.config(provider_tunnel_ip=mock.sentinel.tunneling_ip, + group='NVGRE') + actual = self.agent.get_agent_configurations() + + self.assertEqual(self.agent._physical_network_mappings, + actual['vswitch_mappings']) + self.assertEqual([constants.TYPE_NVGRE], actual['tunnel_types']) + self.assertEqual(mock.sentinel.tunneling_ip, actual['tunneling_ip']) + def test_get_network_vswitch_map_by_port_id(self): net_uuid = 'net-uuid' self.agent._network_vswitch_map = { @@ -78,6 +132,14 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.assertIsNone(network) self.assertIsNone(port_map) + def test_lookup_update(self): + kwargs = {'lookup_ip': mock.sentinel.lookup_ip, + 'lookup_details': mock.sentinel.lookup_details} + + self.agent.lookup_update(mock.sentinel.context, **kwargs) + + self.agent._nvgre_ops.lookup_update.assert_called_once_with(kwargs) + @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, "_get_vswitch_name") def test_provision_network_exception(self, mock_get_vswitch_name): @@ -108,6 +170,25 @@ class TestHyperVNeutronAgent(base.BaseTestCase): mock.sentinel.FAKE_SEGMENTATION_ID, constants.TRUNK_ENDPOINT_MODE) + @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, + "_get_vswitch_name") + def test_provision_network_nvgre(self, mock_get_vswitch_name): + self.agent._nvgre_enabled = True + vswitch_name = mock_get_vswitch_name.return_value + self.agent._provision_network(mock.sentinel.FAKE_PORT_ID, + mock.sentinel.FAKE_NET_UUID, + constants.TYPE_NVGRE, + mock.sentinel.FAKE_PHYSICAL_NETWORK, + mock.sentinel.FAKE_SEGMENTATION_ID) + + mock_get_vswitch_name.assert_called_once_with( + constants.TYPE_NVGRE, + mock.sentinel.FAKE_PHYSICAL_NETWORK) + self.agent._nvgre_ops.bind_nvgre_network.assert_called_once_with( + mock.sentinel.FAKE_SEGMENTATION_ID, + mock.sentinel.FAKE_NET_UUID, + vswitch_name) + @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, "_get_vswitch_name") def test_provision_network_flat(self, mock_get_vswitch_name): @@ -155,6 +236,34 @@ class TestHyperVNeutronAgent(base.BaseTestCase): self.assertEqual(enable_metrics, mock_enable_metrics.called) + @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, + '_provision_network') + def test_port_bound_nvgre(self, mock_provision_network): + self.agent._nvgre_enabled = True + network_type = constants.TYPE_NVGRE + net_uuid = 'my-net-uuid' + fake_map = {'vswitch_name': mock.sentinel.vswitch_name, + 'ports': []} + + def fake_prov_network(*args, **kwargs): + self.agent._network_vswitch_map[net_uuid] = fake_map + + mock_provision_network.side_effect = fake_prov_network + + self.agent._port_bound(mock.sentinel.port_id, net_uuid, network_type, + mock.sentinel.physical_network, + mock.sentinel.segmentation_id) + + self.assertIn(mock.sentinel.port_id, fake_map['ports']) + mock_provision_network.assert_called_once_with( + mock.sentinel.port_id, net_uuid, network_type, + mock.sentinel.physical_network, mock.sentinel.segmentation_id) + self.agent._utils.connect_vnic_to_vswitch.assert_called_once_with( + mock.sentinel.vswitch_name, mock.sentinel.port_id) + self.agent._nvgre_ops.bind_nvgre_port.assert_called_once_with( + mock.sentinel.segmentation_id, mock.sentinel.vswitch_name, + mock.sentinel.port_id) + @mock.patch.object(hyperv_neutron_agent.HyperVNeutronAgentMixin, '_get_network_vswitch_map_by_port_id') def _check_port_unbound(self, mock_get_vswitch_map_by_port_id, ports=None, diff --git a/hyperv/tests/unit/neutron/test_mech_hyperv.py b/hyperv/tests/unit/neutron/test_mech_hyperv.py new file mode 100644 index 0000000..e0996c1 --- /dev/null +++ b/hyperv/tests/unit/neutron/test_mech_hyperv.py @@ -0,0 +1,45 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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. + +""" +Unit tests for the Hyper-V Mechanism Driver. +""" + +from hyperv.neutron import constants +from hyperv.neutron.ml2 import mech_hyperv +from hyperv.tests import base + + +class TestHypervMechanismDriver(base.BaseTestCase): + + def setUp(self): + super(TestHypervMechanismDriver, self).setUp() + self.mech_hyperv = mech_hyperv.HypervMechanismDriver() + + def test_get_allowed_network_types(self): + agent = {'configurations': {'tunnel_types': []}} + actual_net_types = self.mech_hyperv.get_allowed_network_types(agent) + + network_types = [constants.TYPE_LOCAL, constants.TYPE_FLAT, + constants.TYPE_VLAN] + self.assertEqual(network_types, actual_net_types) + + def test_get_allowed_network_types_nvgre(self): + agent = {'configurations': {'tunnel_types': [constants.TYPE_NVGRE]}} + actual_net_types = self.mech_hyperv.get_allowed_network_types(agent) + + network_types = [constants.TYPE_LOCAL, constants.TYPE_FLAT, + constants.TYPE_VLAN, constants.TYPE_NVGRE] + self.assertEqual(network_types, actual_net_types) diff --git a/hyperv/tests/unit/neutron/test_neutron_client.py b/hyperv/tests/unit/neutron/test_neutron_client.py new file mode 100644 index 0000000..b6ef5b8 --- /dev/null +++ b/hyperv/tests/unit/neutron/test_neutron_client.py @@ -0,0 +1,133 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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. + +""" +Unit tests for the neutron client. +""" + +import mock + +from hyperv.neutron import constants +from hyperv.neutron import neutron_client +from hyperv.tests import base + + +class TestNeutronClient(base.BaseTestCase): + + _FAKE_CIDR = '10.0.0.0/24' + _FAKE_GATEWAY = '10.0.0.1' + _FAKE_HOST = 'fake_host' + + def setUp(self): + super(TestNeutronClient, self).setUp() + self._neutron = neutron_client.NeutronAPIClient() + self._neutron._client = mock.MagicMock() + + def test_get_network_subnets(self): + self._neutron._client.show_network.return_value = { + 'network': { + 'subnets': [mock.sentinel.fake_subnet] + } + } + + subnets = self._neutron.get_network_subnets(mock.sentinel.net_id) + + self._neutron._client.show_network.assert_called_once_with( + mock.sentinel.net_id) + self.assertEqual([mock.sentinel.fake_subnet], subnets) + + def test_get_network_subnets_exception(self): + self._neutron._client.show_network.side_effect = Exception("Fail") + subnets = self._neutron.get_network_subnets(mock.sentinel.net_id) + self.assertEqual([], subnets) + + def test_get_network_subnet_cidr(self): + self._neutron._client.show_subnet.return_value = { + 'subnet': { + 'cidr': self._FAKE_CIDR, + 'gateway_ip': self._FAKE_GATEWAY, + } + } + + cidr, gw = self._neutron.get_network_subnet_cidr_and_gateway( + mock.sentinel.subnet_id) + + self._neutron._client.show_subnet.assert_called_once_with( + mock.sentinel.subnet_id) + self.assertEqual(self._FAKE_CIDR, cidr) + self.assertEqual(self._FAKE_GATEWAY, gw) + + def test_get_network_subnet_cidr_exception(self): + self._neutron._client.show_subnet.side_effect = Exception("Fail") + cidr, gw = self._neutron.get_network_subnet_cidr_and_gateway( + mock.sentinel.subnet_id) + self.assertIsNone(cidr) + self.assertIsNone(gw) + + def test_get_port_ip_address(self): + self._neutron._client.show_port.return_value = { + 'port': { + 'fixed_ips': [{'ip_address': mock.sentinel.ip_addr}] + } + } + + ip_addr = self._neutron.get_port_ip_address(mock.sentinel.fake_port_id) + + self._neutron._client.show_port.assert_called_once_with( + mock.sentinel.fake_port_id) + self.assertEqual(mock.sentinel.ip_addr, ip_addr) + + def test_get_port_ip_address_exception(self): + self._neutron._client.show_port.side_effect = Exception("Fail") + ip_addr = self._neutron.get_port_ip_address(mock.sentinel.fake_port_id) + self.assertIsNone(ip_addr) + + def test_get_tunneling_agents(self): + non_tunnel_agent = {} + ignored_agent = {'configurations': { + 'tunnel_types': [constants.TYPE_NVGRE]} + } + tunneling_agent = { + 'configurations': {'tunnel_types': [constants.TYPE_NVGRE], + 'tunneling_ip': mock.sentinel.tunneling_ip}, + 'host': self._FAKE_HOST + } + + self._neutron._client.list_agents.return_value = { + 'agents': [non_tunnel_agent, ignored_agent, tunneling_agent] + } + + actual = self._neutron.get_tunneling_agents() + self.assertEqual({self._FAKE_HOST: mock.sentinel.tunneling_ip}, actual) + + def test_get_tunneling_agents_exception(self): + self._neutron._client.list_agents.side_effect = Exception("Fail") + actual = self._neutron.get_tunneling_agents() + self.assertEqual({}, actual) + + def test_get_network_ports(self): + self._neutron._client.list_ports.return_value = { + 'ports': [mock.sentinel.port] + } + + actual = self._neutron.get_network_ports(key='value') + + self._neutron._client.list_ports.assert_called_once_with(key='value') + self.assertEqual([mock.sentinel.port], actual) + + def test_get_network_ports_exception(self): + self._neutron._client.list_ports.side_effect = Exception("Fail") + actual = self._neutron.get_network_ports() + self.assertEqual([], actual) diff --git a/hyperv/tests/unit/neutron/test_nvgre_ops.py b/hyperv/tests/unit/neutron/test_nvgre_ops.py new file mode 100644 index 0000000..377b430 --- /dev/null +++ b/hyperv/tests/unit/neutron/test_nvgre_ops.py @@ -0,0 +1,183 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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. + +""" +Unit tests for Windows Hyper-V NVGRE driver. +""" + +import mock + +from hyperv.neutron import constants +from hyperv.neutron import nvgre_ops +from hyperv.neutron import utilsfactory +from hyperv.tests import base + + +class TestHyperVNvgreOps(base.BaseTestCase): + + FAKE_MAC_ADDR = 'fa:ke:ma:ca:dd:re:ss' + + def setUp(self): + super(TestHyperVNvgreOps, self).setUp() + + utilsfactory._get_windows_version = mock.MagicMock( + return_value='6.2.0') + + self.context = 'context' + self.ops = nvgre_ops.HyperVNvgreOps([]) + self.ops._vswitch_ips[mock.sentinel.network_name] = ( + mock.sentinel.ip_addr) + self.ops.context = self.context + self.ops._notifier = mock.MagicMock() + self.ops._hyperv_utils = mock.MagicMock() + self.ops._nvgre_utils = mock.MagicMock() + self.ops._n_client = mock.MagicMock() + self.ops._db = mock.MagicMock() + + def test_refresh_tunneling_agents(self): + self.ops._n_client.get_tunneling_agents.return_value = { + mock.sentinel.host: mock.sentinel.host_ip + } + self.ops._refresh_tunneling_agents() + self.assertEqual(mock.sentinel.host_ip, + self.ops._tunneling_agents[mock.sentinel.host]) + + @mock.patch.object(nvgre_ops.HyperVNvgreOps, '_register_lookup_record') + def test_lookup_update(self, mock_register_record): + args = {'lookup_ip': mock.sentinel.lookup_ip, + 'lookup_details': { + 'customer_addr': mock.sentinel.customer_addr, + 'mac_addr': mock.sentinel.mac_addr, + 'customer_vsid': mock.sentinel.vsid} + } + + self.ops.lookup_update(args) + + mock_register_record.assert_called_once_with( + mock.sentinel.lookup_ip, + mock.sentinel.customer_addr, + mock.sentinel.mac_addr, + mock.sentinel.vsid) + + @mock.patch.object(nvgre_ops.HyperVNvgreOps, '_register_lookup_record') + def test_lookup_update_no_details(self, mock_register_record): + self.ops.lookup_update({}) + self.assertFalse(mock_register_record.called) + + def test_register_lookup_record(self): + self.ops._register_lookup_record( + mock.sentinel.provider_addr, mock.sentinel.customer_addr, + mock.sentinel.mac_addr, mock.sentinel.vsid) + + self.ops._nvgre_utils.create_lookup_record.assert_called_once_with( + mock.sentinel.provider_addr, mock.sentinel.customer_addr, + mock.sentinel.mac_addr, mock.sentinel.vsid) + + @mock.patch.object(nvgre_ops.HyperVNvgreOps, '_register_lookup_record') + def test_bind_nvgre_port(self, mock_register_record): + self.ops._nvgre_utils.get_network_iface_ip.return_value = ( + mock.sentinel.provider_addr, mock.sentinel.prefix_len) + + mac_addr = self.ops._hyperv_utils.get_vnic_mac_address.return_value + customer_addr = self.ops._n_client.get_port_ip_address.return_value + + self.ops.bind_nvgre_port(mock.sentinel.vsid, + mock.sentinel.network_name, + mock.sentinel.port_id) + + self.ops._hyperv_utils.set_vswitch_port_vsid.assert_called_once_with( + mock.sentinel.vsid, mock.sentinel.port_id) + mock_register_record.assert_has_calls([ + mock.call(mock.sentinel.provider_addr, customer_addr, mac_addr, + mock.sentinel.vsid), + mock.call(mock.sentinel.ip_addr, constants.IPV4_DEFAULT, mac_addr, + mock.sentinel.vsid)]) + self.ops._notifier.lookup_update.assert_called_once_with( + self.context, mock.sentinel.provider_addr, { + 'customer_addr': customer_addr, + 'mac_addr': mac_addr, + 'customer_vsid': mock.sentinel.vsid + }) + + def test_bind_nvgre_port_no_provider_addr(self): + self.ops._nvgre_utils.get_network_iface_ip = mock.MagicMock( + return_value=(None, None)) + + self.ops.bind_nvgre_port(mock.sentinel.vsid, + mock.sentinel.network_name, + mock.sentinel.port_id) + + self.assertFalse(self.ops._hyperv_utils.set_vswitch_port_vsid.called) + + @mock.patch.object(nvgre_ops.HyperVNvgreOps, 'refresh_nvgre_records') + def test_bind_nvgre_network(self, mock_refresh_records): + self.config(provider_tunnel_ip=mock.sentinel.ip_addr, group='NVGRE') + self.ops._n_client.get_network_subnets.return_value = [ + mock.sentinel.subnet, mock.sentinel.subnet2] + + get_cidr = self.ops._n_client.get_network_subnet_cidr_and_gateway + get_cidr.return_value = (mock.sentinel.cidr, mock.sentinel.gateway) + + self.ops.bind_nvgre_network( + mock.sentinel.vsid, mock.sentinel.net_uuid, + mock.sentinel.vswitch_name) + + self.assertEqual(mock.sentinel.vsid, + self.ops._network_vsids[mock.sentinel.net_uuid]) + self.ops._n_client.get_network_subnets.assert_called_once_with( + mock.sentinel.net_uuid) + get_cidr.assert_called_once_with(mock.sentinel.subnet) + self.ops._nvgre_utils.create_customer_routes.assert_called_once_with( + mock.sentinel.vsid, mock.sentinel.vswitch_name, + mock.sentinel.cidr, mock.sentinel.gateway) + mock_refresh_records.assert_called_once_with( + network_id=mock.sentinel.net_uuid) + self.ops._notifier.tunnel_update.assert_called_once_with( + self.context, mock.sentinel.ip_addr, mock.sentinel.vsid) + + @mock.patch.object(nvgre_ops.HyperVNvgreOps, '_register_lookup_record') + def test_refresh_nvgre_records(self, mock_register_record): + self.ops._nvgre_ports.append(mock.sentinel.processed_port_id) + self.ops._tunneling_agents[mock.sentinel.host_id] = ( + mock.sentinel.agent_ip) + self.ops._network_vsids[mock.sentinel.net_id] = ( + mock.sentinel.vsid) + + processed_port = {'id': mock.sentinel.processed_port_id} + no_host_port = {'id': mock.sentinel.port_no_host_id, + 'binding:host_id': mock.sentinel.odd_host_id} + other_net_id_port = {'id': mock.sentinel.port_other_net_id, + 'binding:host_id': mock.sentinel.host_id, + 'network_id': mock.sentinel.odd_net_id} + port = {'id': mock.sentinel.port_id, + 'binding:host_id': mock.sentinel.host_id, + 'network_id': mock.sentinel.net_id, + 'mac_address': self.FAKE_MAC_ADDR, + 'fixed_ips': [{'ip_address': mock.sentinel.customer_addr}] + } + + self.ops._n_client.get_network_ports.return_value = [ + processed_port, no_host_port, other_net_id_port, port] + + self.ops.refresh_nvgre_records() + + expected_mac = self.FAKE_MAC_ADDR.replace(':', '') + mock_register_record.assert_has_calls([ + mock.call(mock.sentinel.agent_ip, mock.sentinel.customer_addr, + expected_mac, mock.sentinel.vsid), + # mock.call(mock.sentinel.agent_ip, constants.METADATA_ADDR, + # expected_mac, mock.sentinel.vsid) + ]) + self.assertIn(mock.sentinel.port_id, self.ops._nvgre_ports) diff --git a/hyperv/tests/unit/neutron/test_utils.py b/hyperv/tests/unit/neutron/test_utils.py index e24ef30..55d4954 100644 --- a/hyperv/tests/unit/neutron/test_utils.py +++ b/hyperv/tests/unit/neutron/test_utils.py @@ -34,6 +34,15 @@ class HyperVUtilsTestCase(base.BaseTestCase): self.utils = utils.HyperVUtils() self.utils._wmi_conn = mock.MagicMock() + @mock.patch.object(utils.HyperVUtils, '_get_vnic_settings') + def test_get_vnic_mac_address(self, mock_get_vnic_settings): + mock_vnic = mock.MagicMock(Address=mock.sentinel.mac_address) + mock_get_vnic_settings.return_value = mock_vnic + + actual_mac_address = self.utils.get_vnic_mac_address( + mock.sentinel.switch_port_name) + self.assertEqual(mock.sentinel.mac_address, actual_mac_address) + @mock.patch.object(utils.HyperVUtils, "_get_switch_port_path_by_name") def test_disconnect_switch_port_not_found(self, mock_get_swp_path): mock_svc = self.utils._conn.Msvm_VirtualSwitchManagementService()[0] diff --git a/hyperv/tests/unit/neutron/test_utils_nvgre.py b/hyperv/tests/unit/neutron/test_utils_nvgre.py new file mode 100644 index 0000000..4866786 --- /dev/null +++ b/hyperv/tests/unit/neutron/test_utils_nvgre.py @@ -0,0 +1,278 @@ +# Copyright 2015 Cloudbase Solutions SRL +# 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. + +""" +Unit tests for the Hyper-V NVGRE support. +""" + +import mock +from oslo_config import cfg + +from hyperv.neutron import constants +from hyperv.neutron import utils_nvgre +from hyperv.tests import base + +CONF = cfg.CONF + + +class TestHyperVNvgreUtils(base.BaseTestCase): + + _FAKE_RDID = 'fake_rdid' + _FAKE_NETWORK_NAME = 'fake_network_name' + _FAKE_VSID = 9001 + _FAKE_DEST_PREFIX = 'fake_dest_prefix' + _FAKE_GW_BAD = '10.0.0.1' + _FAKE_GW = '10.0.0.2' + + def setUp(self): + super(TestHyperVNvgreUtils, self).setUp() + self.utils = utils_nvgre.NvgreUtils() + self.utils._utils = mock.MagicMock() + self.utils._scimv2 = mock.MagicMock() + + def _create_mock_binding(self): + binding = mock.MagicMock() + binding.BindName = self.utils._WNV_BIND_NAME + binding.Name = mock.sentinel.fake_network + + net_binds = self.utils._scimv2.MSFT_NetAdapterBindingSettingData + net_binds.return_value = [binding] + return binding + + @mock.patch.object(utils_nvgre.NvgreUtils, 'get_network_iface_ip') + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_address(self, mock_get_iface_index, + mock_get_iface_ip): + mock_get_iface_index.return_value = mock.sentinel.iface_index + mock_get_iface_ip.return_value = (mock.sentinel.iface_ip, + mock.sentinel.prefix_len) + + provider_addr = mock.MagicMock() + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationProviderAddressSettingData + obj_class.return_value = [provider_addr] + + self.utils.create_provider_address(mock.sentinel.fake_network, + mock.sentinel.fake_vlan_id) + + self.assertTrue(provider_addr.Delete_.called) + obj_class.new.assert_called_once_with( + ProviderAddress=mock.sentinel.iface_ip, + VlanID=mock.sentinel.fake_vlan_id, + InterfaceIndex=mock.sentinel.iface_index, + PrefixLength=mock.sentinel.prefix_len) + + @mock.patch.object(utils_nvgre.NvgreUtils, 'get_network_iface_ip') + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_address_none(self, mock_get_iface_index, + mock_get_iface_ip): + mock_get_iface_ip.return_value = (None, None) + + self.utils.create_provider_address(mock.sentinel.fake_network, + mock.sentinel.fake_vlan_id) + scimv2 = self.utils._scimv2 + self.assertFalse( + scimv2.MSFT_NetVirtualizationProviderAddressSettingData.new.called) + + @mock.patch.object(utils_nvgre.NvgreUtils, 'get_network_iface_ip') + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_address_exists(self, mock_get_iface_index, + mock_get_iface_ip): + mock_get_iface_index.return_value = mock.sentinel.iface_index + mock_get_iface_ip.return_value = (mock.sentinel.iface_ip, + mock.sentinel.prefix_len) + + provider_addr = mock.MagicMock( + VlanID=mock.sentinel.fake_vlan_id, + InterfaceIndex=mock.sentinel.iface_index) + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationProviderAddressSettingData + obj_class.return_value = [provider_addr] + + self.utils.create_provider_address(mock.sentinel.fake_network, + mock.sentinel.fake_vlan_id) + + self.assertFalse(obj_class.new.called) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_route(self, mock_get_iface_index): + mock_get_iface_index.return_value = mock.sentinel.iface_index + self.utils._scimv2.MSFT_NetVirtualizationProviderRouteSettingData = ( + mock.MagicMock(return_value=[])) + + self.utils.create_provider_route(mock.sentinel.fake_network) + + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationProviderRouteSettingData + obj_class.new.assert_called_once_with( + InterfaceIndex=mock.sentinel.iface_index, + DestinationPrefix='%s/0' % constants.IPV4_DEFAULT, + NextHop=constants.IPV4_DEFAULT) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_route_none(self, mock_get_iface_index): + mock_get_iface_index.return_value = None + + self.utils.create_provider_route(mock.sentinel.fake_network) + scimv2 = self.utils._scimv2 + self.assertFalse( + scimv2.MSFT_NetVirtualizationProviderRouteSettingData.new.called) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_iface_index') + def test_create_provider_route_exists(self, mock_get_iface_index): + mock_get_iface_index.return_value = mock.sentinel.iface_index + self.utils._scimv2.MSFT_NetVirtualizationProviderRouteSettingData = ( + mock.MagicMock(return_value=[mock.MagicMock()])) + + self.utils.create_provider_route(mock.sentinel.fake_network) + + scimv2 = self.utils._scimv2 + self.assertFalse( + scimv2.MSFT_NetVirtualizationProviderRouteSettingData.new.called) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_create_cust_route') + def _check_create_customer_routes(self, mock_create_route, gateway=None): + customer_route = mock.MagicMock() + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationCustomerRouteSettingData + obj_class.return_value = [customer_route] + + self.utils.create_customer_routes( + self._FAKE_VSID, self._FAKE_NETWORK_NAME, self._FAKE_DEST_PREFIX, + gateway) + + routes = [(self._FAKE_DEST_PREFIX, constants.IPV4_DEFAULT)] + if gateway and gateway[-1] != '1': + routes.append(('%s/0' % constants.IPV4_DEFAULT, gateway)) + routes.append(('%s/32' % CONF.AGENT.neutron_metadata_address, + gateway)) + expected_calls = [ + mock.call(self._FAKE_VSID, dest_prefix, next_hop, mock.ANY) + for dest_prefix, next_hop in routes] + + self.assertTrue(customer_route.Delete_.called) + mock_create_route.assert_has_calls(expected_calls) + + def test_create_customer_route(self): + self._check_create_customer_routes(gateway=self._FAKE_GW) + + def test_create_customer_route_no_gateway(self): + self._check_create_customer_routes() + + def test_create_customer_route_bad_gateway(self): + self._check_create_customer_routes(gateway=self._FAKE_GW_BAD) + + def test_create_cust_route(self): + self.utils._create_cust_route( + mock.sentinel.fake_vsid, mock.sentinel.dest_prefix, + mock.sentinel.next_hop, self._FAKE_RDID) + + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationCustomerRouteSettingData + obj_class.new.assert_called_once_with( + VirtualSubnetID=mock.sentinel.fake_vsid, + DestinationPrefix=mock.sentinel.dest_prefix, + NextHop=mock.sentinel.next_hop, + Metric=255, + RoutingDomainID='{%s}' % self._FAKE_RDID) + + def _check_create_lookup_record(self, customer_addr, expected_type): + lookup = mock.MagicMock() + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationLookupRecordSettingData + obj_class.return_value = [lookup] + + self.utils.create_lookup_record(mock.sentinel.provider_addr, + customer_addr, + mock.sentinel.mac_addr, + mock.sentinel.fake_vsid) + + self.assertTrue(lookup.Delete_.called) + obj_class.new.assert_called_once_with( + VirtualSubnetID=mock.sentinel.fake_vsid, + Rule=self.utils._TRANSLATE_ENCAP, + Type=expected_type, + MACAddress=mock.sentinel.mac_addr, + CustomerAddress=customer_addr, + ProviderAddress=mock.sentinel.provider_addr) + + def test_create_lookup_record_l2_only(self): + self._check_create_lookup_record( + constants.IPV4_DEFAULT, + self.utils._LOOKUP_RECORD_TYPE_L2_ONLY) + + def test_create_lookup_record_static(self): + self._check_create_lookup_record( + mock.sentinel.customer_addr, + self.utils._LOOKUP_RECORD_TYPE_STATIC) + + def test_create_lookup_record_exists(self): + lookup = mock.MagicMock(VirtualSubnetID=mock.sentinel.fake_vsid, + ProviderAddress=mock.sentinel.provider_addr, + CustomerAddress=mock.sentinel.customer_addr, + MACAddress=mock.sentinel.mac_addr) + scimv2 = self.utils._scimv2 + obj_class = scimv2.MSFT_NetVirtualizationLookupRecordSettingData + obj_class.return_value = [lookup] + + self.utils.create_lookup_record(mock.sentinel.provider_addr, + mock.sentinel.customer_addr, + mock.sentinel.mac_addr, + mock.sentinel.fake_vsid) + self.assertFalse(obj_class.new.called) + + def test_get_network_iface_index(self): + fake_network = mock.MagicMock(InterfaceIndex=mock.sentinel.iface_index) + self.utils._scimv2.MSFT_NetAdapter.return_value = [fake_network] + description = ( + self.utils._utils.get_vswitch_external_network_name.return_value) + + index = self.utils._get_network_iface_index(mock.sentinel.fake_network) + + self.assertEqual(mock.sentinel.iface_index, index) + self.assertIn(mock.sentinel.fake_network, self.utils._net_if_indexes) + self.utils._scimv2.MSFT_NetAdapter.assert_called_once_with( + InterfaceDescription=description) + + def test_get_network_iface_index_cached(self): + self.utils._net_if_indexes[mock.sentinel.fake_network] = ( + mock.sentinel.iface_index) + + index = self.utils._get_network_iface_index(mock.sentinel.fake_network) + + self.assertEqual(mock.sentinel.iface_index, index) + self.assertFalse(self.utils._scimv2.MSFT_NetAdapter.called) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_ifaces_by_name') + def test_get_network_iface_ip(self, mock_get_net_ifaces): + fake_network = mock.MagicMock( + InterfaceIndex=mock.sentinel.iface_index, + DriverDescription=self.utils._HYPERV_VIRT_ADAPTER) + mock_get_net_ifaces.return_value = [fake_network] + + fake_netip = mock.MagicMock(IPAddress=mock.sentinel.provider_addr, + PrefixLength=mock.sentinel.prefix_len) + self.utils._scimv2.MSFT_NetIPAddress.return_value = [fake_netip] + + pair = self.utils.get_network_iface_ip(mock.sentinel.fake_network) + + self.assertEqual( + (mock.sentinel.provider_addr, mock.sentinel.prefix_len), pair) + + @mock.patch.object(utils_nvgre.NvgreUtils, '_get_network_ifaces_by_name') + def test_get_network_iface_ip_none(self, mock_get_net_ifaces): + mock_get_net_ifaces.return_value = [] + pair = self.utils.get_network_iface_ip(mock.sentinel.fake_network) + self.assertEqual((None, None), pair) diff --git a/hyperv/tests/unit/neutron/test_utilsv2.py b/hyperv/tests/unit/neutron/test_utilsv2.py index f2ebde0..6067d28 100644 --- a/hyperv/tests/unit/neutron/test_utilsv2.py +++ b/hyperv/tests/unit/neutron/test_utilsv2.py @@ -190,6 +190,29 @@ class TestHyperVUtilsV2(base.BaseTestCase): self.assertRaises(utils.HyperVException, self._utils._get_vswitch, self._FAKE_VSWITCH_NAME) + def test_get_vswitch_external_port(self): + vswitch = mock.MagicMock(Name=mock.sentinel.vswitch_name) + self._utils._conn.Msvm_VirtualEthernetSwitch.return_value = [vswitch] + + ext_port = mock.MagicMock() + lan_endpoint1 = mock.MagicMock() + ext_port.associators.return_value = [lan_endpoint1] + lan_endpoint2 = mock.MagicMock(SystemName=mock.sentinel.vswitch_name) + lan_endpoint1.associators.return_value = [lan_endpoint2] + + self._utils._conn.Msvm_ExternalEthernetPort.return_value = [ext_port] + + result = self._utils._get_vswitch_external_port(mock.sentinel.name) + self.assertEqual(ext_port, result) + + @mock.patch.object(utilsv2.HyperVUtilsV2, '_get_vswitch_external_port') + def test_get_vswitch_external_network_name(self, mock_get_vswitch_port): + mock_get_vswitch_port.return_value.ElementName = ( + mock.sentinel.network_name) + result = self._utils.get_vswitch_external_network_name( + mock.sentinel.vswitch_name) + self.assertEqual(mock.sentinel.network_name, result) + def test_set_vswitch_port_vlan_id(self): self._mock_get_switch_port_alloc(found=True) self._utils._get_vlan_setting_data_from_port_alloc = mock.MagicMock() @@ -226,6 +249,36 @@ class TestHyperVUtilsV2(base.BaseTestCase): self.assertFalse(mock_svc.RemoveFeatureSettings.called) self.assertFalse(mock_svc.AddFeatureSettings.called) + @mock.patch.object(utilsv2.HyperVUtilsV2, '_add_virt_feature') + @mock.patch.object(utilsv2.HyperVUtilsV2, '_remove_virt_feature') + @mock.patch.object(utilsv2.HyperVUtilsV2, '_get_security_setting_data') + def test_set_vswitch_port_vsid(self, mock_get_security_sd, mock_rm_feat, + mock_add_feat): + mock_port_alloc = self._mock_get_switch_port_alloc() + + mock_vsid_settings = mock.MagicMock() + mock_port_alloc.associators.return_value = [mock_vsid_settings] + mock_get_security_sd.return_value = (mock_vsid_settings, True) + + self._utils.set_vswitch_port_vsid(mock.sentinel.vsid, + mock.sentinel.switch_port_name) + + mock_rm_feat.assert_called_once_with(mock_vsid_settings) + mock_add_feat.assert_called_once_with(mock_port_alloc, + mock_vsid_settings) + + @mock.patch.object(utilsv2.HyperVUtilsV2, '_add_virt_feature') + def test_set_vswitch_port_vsid_already_set(self, mock_add_feat): + mock_port_alloc = self._mock_get_switch_port_alloc() + + mock_vsid_settings = mock.MagicMock(VirtualSubnetId=mock.sentinel.vsid) + mock_port_alloc.associators.return_value = (mock_vsid_settings, True) + + self._utils.set_vswitch_port_vsid(mock.sentinel.vsid, + mock.sentinel.switch_port_name) + + self.assertFalse(mock_add_feat.called) + def test_get_setting_data(self): self._utils._get_first_item = mock.MagicMock(return_value=None) diff --git a/requirements.txt b/requirements.txt index ef7ea20..345f46f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ oslo.i18n>=1.5.0 # Apache-2.0 oslo.log>=1.2.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 oslo.utils>=1.4.0 # Apache-2.0 + +python-neutronclient \ No newline at end of file