From 2d9e479eb3d5dd3d957fdbccf6d7fd3f5d82ad34 Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Fri, 4 Jan 2013 20:32:09 +0200 Subject: [PATCH] Adds a Hyper-V Quantum plugin Blueprint quantum-plugin-hyper-v Initial Hyper-V Quantum plugin including VLAN support. Support for NVGRE networking will be added in a subsequent patch. The plugin architecture relies heavily on the OVS plugin, with some design differences to handle different network types via polymorphism. The plugin contains two main components: The plugin itself, to be executed on Linux or Windows The L2 agent, to be executed on each Hyper-V node L3 networking is currently handled on Linux with the existing agents. A Nova Quantum Vif plugin is included in the Nova project. Change-Id: Ie64bff448e3fb1129c5e24baaf148cdcc0aed8b9 --- .../plugins/hyperv/hyperv_quantum_plugin.ini | 82 ++++ quantum/extensions/portbindings.py | 1 + quantum/plugins/hyperv/__init__.py | 16 + quantum/plugins/hyperv/agent/__init__.py | 16 + .../hyperv/agent/hyperv_quantum_agent.py | 353 ++++++++++++++++ quantum/plugins/hyperv/agent/utils.py | 283 +++++++++++++ quantum/plugins/hyperv/agent_notifier_api.py | 93 ++++ quantum/plugins/hyperv/common/__init__.py | 16 + quantum/plugins/hyperv/common/constants.py | 32 ++ quantum/plugins/hyperv/db.py | 215 ++++++++++ .../plugins/hyperv/hyperv_quantum_plugin.py | 398 ++++++++++++++++++ quantum/plugins/hyperv/model.py | 55 +++ quantum/plugins/hyperv/rpc_callbacks.py | 102 +++++ quantum/tests/unit/hyperv/__init__.py | 16 + .../unit/hyperv/test_hyperv_quantum_agent.py | 113 +++++ .../unit/hyperv/test_hyperv_quantum_plugin.py | 88 ++++ .../tests/unit/hyperv/test_hyperv_rpcapi.py | 126 ++++++ 17 files changed, 2005 insertions(+) create mode 100644 etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini create mode 100644 quantum/plugins/hyperv/__init__.py create mode 100644 quantum/plugins/hyperv/agent/__init__.py create mode 100644 quantum/plugins/hyperv/agent/hyperv_quantum_agent.py create mode 100644 quantum/plugins/hyperv/agent/utils.py create mode 100644 quantum/plugins/hyperv/agent_notifier_api.py create mode 100644 quantum/plugins/hyperv/common/__init__.py create mode 100644 quantum/plugins/hyperv/common/constants.py create mode 100644 quantum/plugins/hyperv/db.py create mode 100644 quantum/plugins/hyperv/hyperv_quantum_plugin.py create mode 100644 quantum/plugins/hyperv/model.py create mode 100644 quantum/plugins/hyperv/rpc_callbacks.py create mode 100644 quantum/tests/unit/hyperv/__init__.py create mode 100644 quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py create mode 100644 quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py create mode 100644 quantum/tests/unit/hyperv/test_hyperv_rpcapi.py diff --git a/etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini b/etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini new file mode 100644 index 0000000000..97963c8026 --- /dev/null +++ b/etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini @@ -0,0 +1,82 @@ +[DATABASE] +# This line MUST be changed to actually run the plugin. +# Example: +# sql_connection = mysql://quantum:password@127.0.0.1:3306/hyperv_quantum +# Replace 127.0.0.1 above with the IP address of the database used by the +# main quantum server. (Leave it as is if the database runs on this host.) +sql_connection = sqlite:// +# Database reconnection retry times - in event connectivity is lost +# set to -1 implies an infinite retry count +# sql_max_retries = 10 +# Database reconnection interval in seconds - if the initial connection to the +# database fails +reconnect_interval = 2 +# Enable the use of eventlet's db_pool for MySQL. The flags sql_min_pool_size, +# sql_max_pool_size and sql_idle_timeout are relevant only if this is enabled. +# sql_dbpool_enable = False +# Minimum number of SQL connections to keep open in a pool +# sql_min_pool_size = 1 +# Maximum number of SQL connections to keep open in a pool +# sql_max_pool_size = 5 +# Timeout in seconds before idle sql connections are reaped +# sql_idle_timeout = 3600 + +[HYPERV] +# (StrOpt) Type of network to allocate for tenant networks. The +# default value 'local' is useful only for single-box testing and +# provides no connectivity between hosts. You MUST either change this +# to 'vlan' and configure network_vlan_ranges below or to 'flat'. +# Set to 'none' to disable creation of tenant networks. +# +# Default: tenant_network_type = local +# Example: tenant_network_type = vlan + +# (ListOpt) Comma-separated list of +# [::] tuples enumerating ranges +# of VLAN IDs on named physical networks that are available for +# allocation. All physical networks listed are available for flat and +# VLAN provider network creation. Specified ranges of VLAN IDs are +# available for tenant network allocation if tenant_network_type is +# 'vlan'. If empty, only gre and local networks may be created. +# +# Default: network_vlan_ranges = +# Example: network_vlan_ranges = physnet1:1000:2999 + +[AGENT] +# Agent's polling interval in seconds +polling_interval = 2 + +# (ListOpt) Comma separated list of : +# where the physical networks can be expressed with wildcards, +# e.g.: ."*:external". +# The referred external virtual switches need to be already present on +# the Hyper-V server. +# If a given physical network name will not match any value in the list +# the plugin will look for a virtual switch with the same name. +# +# Default: physical_network_vswitch_mappings = *:external +# Example: physical_network_vswitch_mappings = net1:external1,net2:external2 + +# (StrOpt) Private virtual switch name used for local networking. +# +# Default: local_network_vswitch = private +# Example: local_network_vswitch = custom_vswitch + +#----------------------------------------------------------------------------- +# Sample Configurations. +#----------------------------------------------------------------------------- +# +# Quantum server: +# +# [DATABASE] +# sql_connection = mysql://root:nova@127.0.0.1:3306/hyperv_quantum +# [HYPERV] +# tenant_network_type = vlan +# network_vlan_ranges = default:2000:3999 +# +# Agent running on Hyper-V node: +# +# [AGENT] +# polling_interval = 2 +# physical_network_vswitch_mappings = *:external +# local_network_vswitch = private diff --git a/quantum/extensions/portbindings.py b/quantum/extensions/portbindings.py index 5368985114..23980d32c4 100644 --- a/quantum/extensions/portbindings.py +++ b/quantum/extensions/portbindings.py @@ -35,6 +35,7 @@ VIF_TYPE_OVS = 'ovs' VIF_TYPE_BRIDGE = 'bridge' VIF_TYPE_802_QBG = '802.1qbg' VIF_TYPE_802_QBH = '802.1qbh' +VIF_TYPE_HYPERV = 'hyperv' VIF_TYPE_OTHER = 'other' EXTENDED_ATTRIBUTES_2_0 = { diff --git a/quantum/plugins/hyperv/__init__.py b/quantum/plugins/hyperv/__init__.py new file mode 100644 index 0000000000..7ef4e09fa4 --- /dev/null +++ b/quantum/plugins/hyperv/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/quantum/plugins/hyperv/agent/__init__.py b/quantum/plugins/hyperv/agent/__init__.py new file mode 100644 index 0000000000..7ef4e09fa4 --- /dev/null +++ b/quantum/plugins/hyperv/agent/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/quantum/plugins/hyperv/agent/hyperv_quantum_agent.py b/quantum/plugins/hyperv/agent/hyperv_quantum_agent.py new file mode 100644 index 0000000000..687da722d9 --- /dev/null +++ b/quantum/plugins/hyperv/agent/hyperv_quantum_agent.py @@ -0,0 +1,353 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +#Copyright 2013 Cloudbase Solutions SRL +#Copyright 2013 Pedro Navarro Perez +#All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Pedro Navarro Perez +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +import eventlet +import platform +import re +import sys +import time + +from quantum.agent import rpc as agent_rpc +from quantum.common import config as logging_config +from quantum.common import topics +from quantum import context +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging +from quantum.openstack.common.rpc import dispatcher +from quantum.plugins.hyperv.agent import utils +from quantum.plugins.hyperv.common import constants + +LOG = logging.getLogger(__name__) + +agent_opts = [ + cfg.ListOpt( + 'physical_network_vswitch_mappings', + default=[], + help=_('List of : ' + 'where the physical networks can be expressed with ' + 'wildcards, e.g.: ."*:external"')), + cfg.StrOpt( + 'local_network_vswitch', + default='private', + help=_('Private vswitch name used for local networks')), + cfg.IntOpt('polling_interval', default=2), +] + + +CONF = cfg.CONF +CONF.register_opts(agent_opts, "AGENT") + + +class HyperVQuantumAgent(object): + # Set RPC API version to 1.0 by default. + RPC_API_VERSION = '1.0' + + def __init__(self): + self._utils = utils.HyperVUtils() + self._polling_interval = CONF.AGENT.polling_interval + self._load_physical_network_mappings() + self._network_vswitch_map = {} + self._setup_rpc() + + def _setup_rpc(self): + self.agent_id = 'hyperv_%s' % platform.node() + self.topic = topics.AGENT + self.plugin_rpc = agent_rpc.PluginApi(topics.PLUGIN) + + # RPC network init + self.context = context.get_admin_context_without_session() + # Handle updates from service + self.dispatcher = self._create_rpc_dispatcher() + # Define the listening consumers for the agent + consumers = [[topics.PORT, topics.UPDATE], + [topics.NETWORK, topics.DELETE], + [topics.PORT, topics.DELETE], + [constants.TUNNEL, topics.UPDATE]] + self.connection = agent_rpc.create_consumers(self.dispatcher, + self.topic, + consumers) + + def _load_physical_network_mappings(self): + self._physical_network_mappings = {} + for mapping in CONF.AGENT.physical_network_vswitch_mappings: + parts = mapping.split(':') + if len(parts) != 2: + LOG.debug(_('Invalid physical network mapping: %s'), mapping) + else: + pattern = re.escape(parts[0].strip()).replace('\\*', '.*') + vswitch = parts[1].strip() + self._physical_network_mappings[re.compile(pattern)] = vswitch + + def _get_vswitch_for_physical_network(self, phys_network_name): + for compre in self._physical_network_mappings: + if phys_network_name is None: + phys_network_name = '' + if compre.match(phys_network_name): + return self._physical_network_mappings[compre] + # Not found in the mappings, the vswitch has the same name + return phys_network_name + + def _get_network_vswitch_map_by_port_id(self, port_id): + for network_id, map in self._network_vswitch_map.iteritems(): + if port_id in map['ports']: + return (network_id, map) + + def network_delete(self, context, network_id=None): + LOG.debug(_("network_delete received. " + "Deleting network %s"), network_id) + # The network may not be defined on this agent + if network_id in self._network_vswitch_map: + self._reclaim_local_network(network_id) + else: + LOG.debug(_("Network %s not defined on agent."), network_id) + + def port_delete(self, context, port_id=None): + LOG.debug(_("port_delete received")) + self._port_unbound(port_id) + + def port_update(self, context, port=None, network_type=None, + segmentation_id=None, physical_network=None): + LOG.debug(_("port_update received")) + self._treat_vif_port( + port['id'], port['network_id'], + network_type, physical_network, + segmentation_id, port['admin_state_up']) + + def _create_rpc_dispatcher(self): + return dispatcher.RpcDispatcher([self]) + + def _get_vswitch_name(self, network_type, physical_network): + if network_type != constants.TYPE_LOCAL: + vswitch_name = self._get_vswitch_for_physical_network( + physical_network) + else: + vswitch_name = CONF.AGENT.local_network_vswitch + return vswitch_name + + def _provision_network(self, port_id, + net_uuid, network_type, + physical_network, + segmentation_id): + LOG.info(_("Provisioning network %s"), net_uuid) + + vswitch_name = self._get_vswitch_name(network_type, physical_network) + + if network_type == constants.TYPE_VLAN: + self._utils.add_vlan_id_to_vswitch(segmentation_id, vswitch_name) + elif network_type == constants.TYPE_FLAT: + self._utils.set_vswitch_mode_access(vswitch_name) + elif network_type == constants.TYPE_LOCAL: + #TODO (alexpilotti): Check that the switch type is private + #or create it if not existing + pass + else: + raise utils.HyperVException(_("Cannot provision unknown network " + "type %s for network %s"), + network_type, net_uuid) + + map = { + 'network_type': network_type, + 'vswitch_name': vswitch_name, + 'ports': [], + 'vlan_id': segmentation_id} + self._network_vswitch_map[net_uuid] = map + + def _reclaim_local_network(self, net_uuid): + LOG.info(_("Reclaiming local network %s"), net_uuid) + map = self._network_vswitch_map[net_uuid] + + if map['network_type'] == constants.TYPE_VLAN: + LOG.info(_("Reclaiming VLAN ID %s "), map['vlan_id']) + self._utils.remove_vlan_id_from_vswitch( + map['vlan_id'], map['vswitch_name']) + else: + raise utils.HyperVException(_("Cannot reclaim unsupported " + "network type %s for network %s"), + map['network_type'], net_uuid) + + del self._network_vswitch_map[net_uuid] + + def _port_bound(self, port_id, + net_uuid, + network_type, + physical_network, + segmentation_id): + LOG.debug(_("Binding port %s"), port_id) + + if net_uuid not in self._network_vswitch_map: + self._provision_network( + port_id, net_uuid, network_type, + physical_network, segmentation_id) + + map = self._network_vswitch_map[net_uuid] + map['ports'].append(port_id) + + self._utils.connect_vnic_to_vswitch(map['vswitch_name'], port_id) + + if network_type == constants.TYPE_VLAN: + LOG.info(_('Binding VLAN ID %s to switch port %s'), + segmentation_id, port_id) + self._utils.set_vswitch_port_vlan_id( + segmentation_id, + port_id) + elif network_type == constants.TYPE_FLAT: + #Nothing to do + pass + elif network_type == constants.TYPE_LOCAL: + #Nothing to do + pass + else: + LOG.error(_('Unsupported network type %s'), network_type) + + def _port_unbound(self, port_id): + (net_uuid, map) = self._get_network_vswitch_map_by_port_id(port_id) + if not net_uuid in self._network_vswitch_map: + LOG.info(_('Network %s is not avalailable on this agent'), + net_uuid) + return + + LOG.debug(_("Unbinding port %s"), port_id) + self._utils.disconnect_switch_port(map['vswitch_name'], port_id, True) + + if not map['ports']: + self._reclaim_local_network(net_uuid) + + def _update_ports(self, registered_ports): + ports = self._utils.get_vnic_ids() + if ports == registered_ports: + return + added = ports - registered_ports + removed = registered_ports - ports + return {'current': ports, + 'added': added, + 'removed': removed} + + def _treat_vif_port(self, port_id, network_id, network_type, + physical_network, segmentation_id, + admin_state_up): + if self._utils.vnic_port_exists(port_id): + if admin_state_up: + self._port_bound(port_id, network_id, network_type, + physical_network, segmentation_id) + else: + self._port_unbound(port_id) + else: + LOG.debug(_("No port %s defined on agent."), port_id) + + def _treat_devices_added(self, devices): + resync = False + for device in devices: + LOG.info(_("Adding port %s") % device) + try: + device_details = self.plugin_rpc.get_device_details( + self.context, + device, + self.agent_id) + except Exception as e: + LOG.debug(_( + "Unable to get port details for device %s: %s"), + device, e) + resync = True + continue + if 'port_id' in device_details: + LOG.info(_( + "Port %(device)s updated. Details: %(device_details)s") % + locals()) + self._treat_vif_port( + device_details['port_id'], + device_details['network_id'], + device_details['network_type'], + device_details['physical_network'], + device_details['segmentation_id'], + device_details['admin_state_up']) + return resync + + def _treat_devices_removed(self, devices): + resync = False + for device in devices: + LOG.info(_("Removing port %s"), device) + try: + self.plugin_rpc.update_device_down(self.context, + device, + self.agent_id) + except Exception as e: + LOG.debug(_("Removing port failed for device %s: %s"), + device, e) + resync = True + continue + self._port_unbound(device) + return resync + + def _process_network_ports(self, port_info): + resync_a = False + resync_b = False + if 'added' in port_info: + resync_a = self._treat_devices_added(port_info['added']) + if 'removed' in port_info: + resync_b = self._treat_devices_removed(port_info['removed']) + # If one of the above operations fails => resync with plugin + return (resync_a | resync_b) + + def daemon_loop(self): + sync = True + ports = set() + + while True: + try: + start = time.time() + if sync: + LOG.info(_("Agent out of sync with plugin!")) + ports.clear() + sync = False + + port_info = self._update_ports(ports) + + # notify plugin about port deltas + if port_info: + LOG.debug(_("Agent loop has new devices!")) + # If treat devices fails - must resync with plugin + sync = self._process_network_ports(port_info) + ports = port_info['current'] + except Exception as e: + LOG.exception(_("Error in agent event loop: %s"), e) + sync = True + + # sleep till end of polling interval + elapsed = (time.time() - start) + if (elapsed < self._polling_interval): + time.sleep(self._polling_interval - elapsed) + else: + LOG.debug(_("Loop iteration exceeded interval " + "(%(polling_interval)s vs. %(elapsed)s)"), + {'polling_interval': self._polling_interval, + 'elapsed': elapsed}) + + +def main(): + eventlet.monkey_patch() + cfg.CONF(project='quantum') + logging_config.setup_logging(cfg.CONF) + + plugin = HyperVQuantumAgent() + + # Start everything. + LOG.info(_("Agent initialized successfully, now running... ")) + plugin.daemon_loop() + sys.exit(0) diff --git a/quantum/plugins/hyperv/agent/utils.py b/quantum/plugins/hyperv/agent/utils.py new file mode 100644 index 0000000000..244fea379e --- /dev/null +++ b/quantum/plugins/hyperv/agent/utils.py @@ -0,0 +1,283 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Cloudbase Solutions SRL +# Copyright 2013 Pedro Navarro Perez +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Pedro Navarro Perez +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +import sys +import time +import uuid + +from quantum.common import exceptions as q_exc +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging + +# Check needed for unit testing on Unix +if sys.platform == 'win32': + import wmi + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class HyperVException(q_exc.QuantumException): + message = _('HyperVException: %(msg)s') + +SET_ACCESS_MODE = 0 +VLAN_ID_ADD = 1 +VLAN_ID_REMOVE = 2 +ENDPOINT_MODE_ACCESS = 2 +ENDPOINT_MODE_TRUNK = 5 + +WMI_JOB_STATE_RUNNING = 4 +WMI_JOB_STATE_COMPLETED = 7 + + +class HyperVUtils(object): + def __init__(self): + self._wmi_conn = None + + @property + def _conn(self): + if self._wmi_conn is None: + self._wmi_conn = wmi.WMI(moniker='//./root/virtualization') + return self._wmi_conn + + def get_switch_ports(self, vswitch_name): + vswitch = self._get_vswitch(vswitch_name) + vswitch_ports = vswitch.associators( + wmi_result_class='Msvm_SwitchPort') + return set(p.Name for p in vswitch_ports) + + def vnic_port_exists(self, port_id): + try: + self._get_vnic_settings(port_id) + except Exception: + return False + return True + + def get_vnic_ids(self): + return set( + p.ElementName + for p in self._conn.Msvm_SyntheticEthernetPortSettingData()) + + def _get_vnic_settings(self, vnic_name): + vnic_settings = self._conn.Msvm_SyntheticEthernetPortSettingData( + ElementName=vnic_name) + if not len(vnic_settings): + raise HyperVException(msg=_('Vnic not found: %s') % vnic_name) + return vnic_settings[0] + + def connect_vnic_to_vswitch(self, vswitch_name, switch_port_name): + vnic_settings = self._get_vnic_settings(switch_port_name) + if not vnic_settings.Connection or not vnic_settings.Connection[0]: + port = self.get_port_by_id(switch_port_name, vswitch_name) + if port: + port_path = port.Path_() + else: + port_path = self._create_switch_port( + vswitch_name, switch_port_name) + vnic_settings.Connection = [port_path] + self._modify_virt_resource(vnic_settings) + + def _get_vm_from_res_setting_data(self, res_setting_data): + sd = res_setting_data.associators( + wmi_result_class='Msvm_VirtualSystemSettingData') + vm = sd[0].associators( + wmi_result_class='Msvm_ComputerSystem') + return vm[0] + + def _modify_virt_resource(self, res_setting_data): + vm = self._get_vm_from_res_setting_data(res_setting_data) + + vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] + (job_path, + ret_val) = vs_man_svc.ModifyVirtualSystemResources( + vm.Path_(), [res_setting_data.GetText_(1)]) + self._check_job_status(ret_val, job_path) + + def _check_job_status(self, ret_val, jobpath): + """Poll WMI job state for completion""" + if not ret_val: + return + elif ret_val != WMI_JOB_STATE_RUNNING: + raise HyperVException(msg=_('Job failed with error %d' % ret_val)) + + job_wmi_path = jobpath.replace('\\', '/') + job = wmi.WMI(moniker=job_wmi_path) + + while job.JobState == WMI_JOB_STATE_RUNNING: + time.sleep(0.1) + job = wmi.WMI(moniker=job_wmi_path) + if job.JobState != WMI_JOB_STATE_COMPLETED: + job_state = job.JobState + if job.path().Class == "Msvm_ConcreteJob": + err_sum_desc = job.ErrorSummaryDescription + err_desc = job.ErrorDescription + err_code = job.ErrorCode + raise HyperVException( + msg=_("WMI job failed with status %(job_state)d. " + "Error details: %(err_sum_desc)s - %(err_desc)s - " + "Error code: %(err_code)d") % locals()) + else: + (error, ret_val) = job.GetError() + if not ret_val and error: + raise HyperVException( + msg=_("WMI job failed with status %(job_state)d. " + "Error details: %(error)s") % locals()) + else: + raise HyperVException( + msg=_("WMI job failed with status %(job_state)d. " + "No error description available") % locals()) + + desc = job.Description + elap = job.ElapsedTime + LOG.debug(_("WMI job succeeded: %(desc)s, Elapsed=%(elap)s") % + locals()) + + def _create_switch_port(self, vswitch_name, switch_port_name): + """ Creates a switch port """ + switch_svc = self._conn.Msvm_VirtualSwitchManagementService()[0] + vswitch_path = self._get_vswitch(vswitch_name).path_() + (new_port, ret_val) = switch_svc.CreateSwitchPort( + Name=switch_port_name, + FriendlyName=switch_port_name, + ScopeOfResidence="", + VirtualSwitch=vswitch_path) + if ret_val != 0: + raise HyperVException( + msg=_('Failed creating port for %s') % vswitch_name) + return new_port + + def disconnect_switch_port( + self, vswitch_name, switch_port_name, delete_port): + """ Disconnects the switch port """ + switch_svc = self._conn.Msvm_VirtualSwitchManagementService()[0] + switch_port_path = self._get_switch_port_path_by_name( + switch_port_name) + if not switch_port_path: + # Port not found. It happens when the VM was already deleted. + return + + (ret_val, ) = switch_svc.DisconnectSwitchPort( + SwitchPort=switch_port_path) + if ret_val != 0: + raise HyperVException( + msg=_('Failed to disconnect port %(switch_port_name)s ' + 'from switch %(vswitch_name)s ' + 'with error %(ret_val)s') % locals()) + if delete_port: + (ret_val, ) = switch_svc.DeleteSwitchPort( + SwitchPort=switch_port_path) + if ret_val != 0: + raise HyperVException( + msg=_('Failed to delete port %(switch_port_name)s ' + 'from switch %(vswitch_name)s ' + 'with error %(ret_val)s') % locals()) + + def _get_vswitch(self, vswitch_name): + vswitch = self._conn.Msvm_VirtualSwitch(ElementName=vswitch_name) + if not len(vswitch): + raise HyperVException(msg=_('VSwitch not found: %s') % + vswitch_name) + return vswitch[0] + + def _get_vswitch_external_port(self, vswitch): + vswitch_ports = vswitch.associators( + wmi_result_class='Msvm_SwitchPort') + for vswitch_port in vswitch_ports: + lan_endpoints = vswitch_port.associators( + wmi_result_class='Msvm_SwitchLanEndpoint') + if len(lan_endpoints): + ext_port = lan_endpoints[0].associators( + wmi_result_class='Msvm_ExternalEthernetPort') + if ext_port: + return vswitch_port + + def _set_vswitch_external_port_vlan_id(self, vswitch_name, action, + vlan_id=None): + vswitch = self._get_vswitch(vswitch_name) + ext_port = self._get_vswitch_external_port(vswitch) + if not ext_port: + return + + vlan_endpoint = ext_port.associators( + wmi_association_class='Msvm_BindsTo')[0] + vlan_endpoint_settings = vlan_endpoint.associators( + wmi_association_class='Msvm_NetworkElementSettingData')[0] + + mode = ENDPOINT_MODE_TRUNK + trunked_vlans = vlan_endpoint_settings.TrunkedVLANList + new_trunked_vlans = trunked_vlans + if action == VLAN_ID_ADD: + if vlan_id not in trunked_vlans: + new_trunked_vlans += (vlan_id,) + elif action == VLAN_ID_REMOVE: + if vlan_id in trunked_vlans: + new_trunked_vlans = [ + v for v in trunked_vlans if v != vlan_id + ] + elif action == SET_ACCESS_MODE: + mode = ENDPOINT_MODE_ACCESS + new_trunked_vlans = () + + if vlan_endpoint.DesiredEndpointMode != mode: + vlan_endpoint.DesiredEndpointMode = mode + vlan_endpoint.put() + + if len(trunked_vlans) != len(new_trunked_vlans): + vlan_endpoint_settings.TrunkedVLANList = new_trunked_vlans + vlan_endpoint_settings.put() + + def set_vswitch_port_vlan_id(self, vlan_id, switch_port_name): + vlan_endpoint_settings = self._conn.Msvm_VLANEndpointSettingData( + ElementName=switch_port_name)[0] + if vlan_endpoint_settings.AccessVLAN != vlan_id: + vlan_endpoint_settings.AccessVLAN = vlan_id + vlan_endpoint_settings.put() + + def set_vswitch_mode_access(self, vswitch_name): + LOG.info(_('Setting vswitch %s in access mode (flat)'), vswitch_name) + self._set_vswitch_external_port_vlan_id(vswitch_name, SET_ACCESS_MODE) + + def add_vlan_id_to_vswitch(self, vlan_id, vswitch_name): + LOG.info(_('Adding VLAN %s to vswitch %s'), + vlan_id, vswitch_name) + self._set_vswitch_external_port_vlan_id(vswitch_name, VLAN_ID_ADD, + vlan_id) + + def remove_vlan_id_from_vswitch(self, vlan_id, vswitch_name): + LOG.info(_('Removing VLAN %s from vswitch %s'), + vlan_id, vswitch_name) + self._set_vswitch_external_port_vlan_id(vswitch_name, VLAN_ID_REMOVE, + vlan_id) + + def _get_switch_port_path_by_name(self, switch_port_name): + vswitch = self._conn.Msvm_SwitchPort(ElementName=switch_port_name) + if vswitch: + return vswitch[0].path_() + + def get_vswitch_id(self, vswitch_name): + vswitch = self._get_vswitch(vswitch_name) + return vswitch.Name + + def get_port_by_id(self, port_id, vswitch_name): + vswitch = self._get_vswitch(vswitch_name) + switch_ports = vswitch.associators(wmi_result_class='Msvm_SwitchPort') + for switch_port in switch_ports: + if (switch_port.ElementName == port_id): + return switch_port diff --git a/quantum/plugins/hyperv/agent_notifier_api.py b/quantum/plugins/hyperv/agent_notifier_api.py new file mode 100644 index 0000000000..e992aa60c2 --- /dev/null +++ b/quantum/plugins/hyperv/agent_notifier_api.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +from quantum.api.v2 import attributes +from quantum.common import constants as q_const +from quantum.common import exceptions as q_exc +from quantum.common import rpc as q_rpc +from quantum.common import topics +from quantum.db import db_base_plugin_v2 +from quantum.db import dhcp_rpc_base +from quantum.db import l3_db +from quantum.db import l3_rpc_base +from quantum.extensions import portbindings +from quantum.extensions import providernet as provider +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging +from quantum.openstack.common import rpc +from quantum.openstack.common.rpc import proxy +from quantum.plugins.hyperv.common import constants +from quantum import policy + +LOG = logging.getLogger(__name__) + + +class AgentNotifierApi(proxy.RpcProxy): + '''Agent side of the openvswitch rpc API. + + API version history: + 1.0 - Initial version. + + ''' + + BASE_RPC_API_VERSION = '1.0' + + def __init__(self, topic): + super(AgentNotifierApi, self).__init__( + topic=topic, default_version=self.BASE_RPC_API_VERSION) + self.topic_network_delete = topics.get_topic_name(topic, + topics.NETWORK, + topics.DELETE) + self.topic_port_update = topics.get_topic_name(topic, + topics.PORT, + topics.UPDATE) + self.topic_port_delete = topics.get_topic_name(topic, + topics.PORT, + topics.DELETE) + self.topic_tunnel_update = topics.get_topic_name(topic, + constants.TUNNEL, + topics.UPDATE) + + def network_delete(self, context, network_id): + self.fanout_cast(context, + self.make_msg('network_delete', + network_id=network_id), + topic=self.topic_network_delete) + + def port_update(self, context, port, network_type, segmentation_id, + physical_network): + self.fanout_cast(context, + self.make_msg('port_update', + port=port, + network_type=network_type, + segmentation_id=segmentation_id, + physical_network=physical_network), + topic=self.topic_port_update) + + def port_delete(self, context, port_id): + self.fanout_cast(context, + self.make_msg('port_delete', + port_id=port_id), + topic=self.topic_port_delete) + + def tunnel_update(self, context, tunnel_ip, tunnel_id): + self.fanout_cast(context, + self.make_msg('tunnel_update', + tunnel_ip=tunnel_ip, + tunnel_id=tunnel_id), + topic=self.topic_tunnel_update) diff --git a/quantum/plugins/hyperv/common/__init__.py b/quantum/plugins/hyperv/common/__init__.py new file mode 100644 index 0000000000..c5618535cf --- /dev/null +++ b/quantum/plugins/hyperv/common/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/quantum/plugins/hyperv/common/constants.py b/quantum/plugins/hyperv/common/constants.py new file mode 100644 index 0000000000..03330fb896 --- /dev/null +++ b/quantum/plugins/hyperv/common/constants.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +# Topic for tunnel notifications between the plugin and agent +TUNNEL = 'tunnel' + +# Special vlan_id value in ovs_vlan_allocations table indicating flat network +FLAT_VLAN_ID = -1 +VLAN_ID_MIN = 1 +VLAN_ID_MAX = 4096 + +# Values for network_type +TYPE_LOCAL = 'local' +TYPE_FLAT = 'flat' +TYPE_VLAN = 'vlan' +TYPE_NVGRE = 'gre' +TYPE_NONE = 'none' diff --git a/quantum/plugins/hyperv/db.py b/quantum/plugins/hyperv/db.py new file mode 100644 index 0000000000..3510417de1 --- /dev/null +++ b/quantum/plugins/hyperv/db.py @@ -0,0 +1,215 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +from sqlalchemy.orm import exc + +from quantum.common import exceptions as q_exc +import quantum.db.api as db_api +from quantum.db import models_v2 +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging +from quantum.plugins.hyperv.common import constants +from quantum.plugins.hyperv import model as hyperv_model + +LOG = logging.getLogger(__name__) + + +class HyperVPluginDB(object): + def initialize(self): + db_api.configure_db() + + def reserve_vlan(self, session): + with session.begin(subtransactions=True): + alloc_q = session.query(hyperv_model.VlanAllocation) + alloc_q = alloc_q.filter_by(allocated=False) + alloc = alloc_q.first() + if alloc: + LOG.debug(_("Reserving vlan %(vlan_id)s on physical network " + "%(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': alloc.physical_network}) + alloc.allocated = True + return (alloc.physical_network, alloc.vlan_id) + raise q_exc.NoNetworkAvailable() + + def reserve_flat_net(self, session): + with session.begin(subtransactions=True): + alloc_q = session.query(hyperv_model.VlanAllocation) + alloc_q = alloc_q.filter_by(allocated=False, + vlan_id=constants.FLAT_VLAN_ID) + alloc = alloc_q.first() + if alloc: + LOG.debug(_("Reserving flat physical network " + "%(physical_network)s from pool"), + {'physical_network': alloc.physical_network}) + alloc.allocated = True + return alloc.physical_network + raise q_exc.NoNetworkAvailable() + + def reserve_specific_vlan(self, session, physical_network, vlan_id): + with session.begin(subtransactions=True): + try: + alloc_q = session.query(hyperv_model.VlanAllocation) + alloc_q = alloc_q.filter_by( + physical_network=physical_network, + vlan_id=vlan_id) + alloc = alloc_q.one() + if alloc.allocated: + if vlan_id == constants.FLAT_VLAN_ID: + raise q_exc.FlatNetworkInUse( + physical_network=physical_network) + else: + raise q_exc.VlanIdInUse( + vlan_id=vlan_id, + physical_network=physical_network) + LOG.debug(_("Reserving specific vlan %(vlan_id)s on physical " + "network %(physical_network)s from pool"), + locals()) + alloc.allocated = True + except exc.NoResultFound: + raise q_exc.NoNetworkAvailable() + + def reserve_specific_flat_net(self, session, physical_network): + return self.reserve_specific_vlan(session, physical_network, + constants.FLAT_VLAN_ID) + + def add_network_binding(self, session, network_id, network_type, + physical_network, segmentation_id): + with session.begin(subtransactions=True): + binding = hyperv_model.NetworkBinding( + network_id, network_type, + physical_network, + segmentation_id) + session.add(binding) + + def get_port(self, port_id): + session = db_api.get_session() + try: + port = session.query(models_v2.Port).filter_by(id=port_id).one() + except exc.NoResultFound: + port = None + return port + + def get_network_binding(self, session, network_id): + session = session or db_api.get_session() + try: + binding_q = session.query(hyperv_model.NetworkBinding) + binding_q = binding_q.filter_by(network_id=network_id) + return binding_q.one() + except exc.NoResultFound: + return + + def set_port_status(self, port_id, status): + session = db_api.get_session() + try: + port = session.query(models_v2.Port).filter_by(id=port_id).one() + port['status'] = status + session.merge(port) + session.flush() + except exc.NoResultFound: + raise q_exc.PortNotFound(port_id=port_id) + + def release_vlan(self, session, physical_network, vlan_id): + with session.begin(subtransactions=True): + try: + alloc_q = session.query(hyperv_model.VlanAllocation) + alloc_q = alloc_q.filter_by(physical_network=physical_network, + vlan_id=vlan_id) + alloc = alloc_q.one() + alloc.allocated = False + #session.delete(alloc) + LOG.debug(_("Releasing vlan %(vlan_id)s on physical network " + "%(physical_network)s"), + locals()) + except exc.NoResultFound: + LOG.warning(_("vlan_id %(vlan_id)s on physical network " + "%(physical_network)s not found"), + locals()) + + def _add_missing_allocatable_vlans(self, session, vlan_ids, + physical_network): + for vlan_id in sorted(vlan_ids): + alloc = hyperv_model.VlanAllocation( + physical_network, vlan_id) + session.add(alloc) + + def _remove_non_allocatable_vlans(self, session, + physical_network, + vlan_ids, + allocations): + if physical_network in allocations: + for alloc in allocations[physical_network]: + try: + # see if vlan is allocatable + vlan_ids.remove(alloc.vlan_id) + except KeyError: + # it's not allocatable, so check if its allocated + if not alloc.allocated: + # it's not, so remove it from table + LOG.debug(_( + "Removing vlan %(vlan_id)s on " + "physical network " + "%(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': physical_network}) + session.delete(alloc) + del allocations[physical_network] + + def _remove_unconfigured_vlans(self, session, allocations): + for allocs in allocations.itervalues(): + for alloc in allocs: + if not alloc.allocated: + LOG.debug(_("Removing vlan %(vlan_id)s on physical " + "network %(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': alloc.physical_network}) + session.delete(alloc) + + def sync_vlan_allocations(self, network_vlan_ranges): + """Synchronize vlan_allocations table with configured VLAN ranges""" + + session = db_api.get_session() + with session.begin(): + # get existing allocations for all physical networks + allocations = dict() + allocs_q = session.query(hyperv_model.VlanAllocation) + for alloc in allocs_q.all(): + allocations.setdefault(alloc.physical_network, + set()).add(alloc) + + # process vlan ranges for each configured physical network + for physical_network, vlan_ranges in network_vlan_ranges.items(): + # determine current configured allocatable vlans for this + # physical network + vlan_ids = set() + for vlan_range in vlan_ranges: + vlan_ids |= set(xrange(vlan_range[0], vlan_range[1] + 1)) + + # remove from table unallocated vlans not currently allocatable + self._remove_non_allocatable_vlans(session, + physical_network, + vlan_ids, + allocations) + + # add missing allocatable vlans to table + self._add_missing_allocatable_vlans(session, vlan_ids, + physical_network) + + # remove from table unallocated vlans for any unconfigured physical + # networks + self._remove_unconfigured_vlans(session, allocations) diff --git a/quantum/plugins/hyperv/hyperv_quantum_plugin.py b/quantum/plugins/hyperv/hyperv_quantum_plugin.py new file mode 100644 index 0000000000..e7bdd8dcb7 --- /dev/null +++ b/quantum/plugins/hyperv/hyperv_quantum_plugin.py @@ -0,0 +1,398 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +import sys + +from quantum.api.v2 import attributes +from quantum.common import constants as q_const +from quantum.common import exceptions as q_exc +from quantum.common import rpc as q_rpc +from quantum.common import topics +from quantum.db import db_base_plugin_v2 +from quantum.db import dhcp_rpc_base +from quantum.db import l3_db +from quantum.db import l3_rpc_base +from quantum.extensions import portbindings +from quantum.extensions import providernet as provider +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging +from quantum.openstack.common import rpc +from quantum.openstack.common.rpc import proxy +from quantum.plugins.hyperv.common import constants +from quantum.plugins.hyperv import db as hyperv_db +from quantum.plugins.hyperv import agent_notifier_api +from quantum.plugins.hyperv import rpc_callbacks +from quantum import policy + +DEFAULT_VLAN_RANGES = [] + +hyperv_opts = [ + cfg.StrOpt('tenant_network_type', default='local', + help=_("Network type for tenant networks " + "(local, flat, vlan or none)")), + cfg.ListOpt('network_vlan_ranges', + default=DEFAULT_VLAN_RANGES, + help=_("List of :: " + "or ")), +] + +cfg.CONF.register_opts(hyperv_opts, "HYPERV") + +LOG = logging.getLogger(__name__) + + +class BaseNetworkProvider(object): + def __init__(self): + self._db = hyperv_db.HyperVPluginDB() + + def create_network(self, session, attrs): + pass + + def delete_network(self, session, binding): + pass + + def extend_network_dict(self, network, binding): + pass + + +class LocalNetworkProvider(BaseNetworkProvider): + def create_network(self, session, attrs): + network_type = attrs.get(provider.NETWORK_TYPE) + segmentation_id = attrs.get(provider.SEGMENTATION_ID) + if attributes.is_attr_set(segmentation_id): + msg = _("segmentation_id specified " + "for %s network") % network_type + raise q_exc.InvalidInput(error_message=msg) + attrs[provider.SEGMENTATION_ID] = None + + physical_network = attrs.get(provider.PHYSICAL_NETWORK) + if attributes.is_attr_set(physical_network): + msg = _("physical_network specified " + "for %s network") % network_type + raise q_exc.InvalidInput(error_message=msg) + attrs[provider.PHYSICAL_NETWORK] = None + + def extend_network_dict(self, network, binding): + network[provider.PHYSICAL_NETWORK] = None + network[provider.SEGMENTATION_ID] = None + + +class FlatNetworkProvider(BaseNetworkProvider): + def create_network(self, session, attrs): + network_type = attrs.get(provider.NETWORK_TYPE) + segmentation_id = attrs.get(provider.SEGMENTATION_ID) + if attributes.is_attr_set(segmentation_id): + msg = _("segmentation_id specified " + "for %s network") % network_type + raise q_exc.InvalidInput(error_message=msg) + segmentation_id = constants.FLAT_VLAN_ID + attrs[provider.SEGMENTATION_ID] = segmentation_id + + physical_network = attrs.get(provider.PHYSICAL_NETWORK) + if not attributes.is_attr_set(physical_network): + physical_network = self._db.reserve_flat_net(session) + attrs[provider.PHYSICAL_NETWORK] = physical_network + else: + self._db.reserve_specific_flat_net(session, physical_network) + + def delete_network(self, session, binding): + self._db.release_vlan(session, binding.physical_network, + constants.FLAT_VLAN_ID) + + def extend_network_dict(self, network, binding): + network[provider.PHYSICAL_NETWORK] = binding.physical_network + + +class VlanNetworkProvider(BaseNetworkProvider): + def create_network(self, session, attrs): + segmentation_id = attrs.get(provider.SEGMENTATION_ID) + if attributes.is_attr_set(segmentation_id): + physical_network = attrs.get(provider.PHYSICAL_NETWORK) + if not attributes.is_attr_set(physical_network): + msg = _("physical_network not provided") + raise q_exc.InvalidInput(error_message=msg) + self._db.reserve_specific_vlan(session, physical_network, + segmentation_id) + else: + (physical_network, + segmentation_id) = self._db.reserve_vlan(session) + attrs[provider.SEGMENTATION_ID] = segmentation_id + attrs[provider.PHYSICAL_NETWORK] = physical_network + + def delete_network(self, session, binding): + self._db.release_vlan( + session, binding.physical_network, + binding.segmentation_id) + + def extend_network_dict(self, network, binding): + network[provider.PHYSICAL_NETWORK] = binding.physical_network + network[provider.SEGMENTATION_ID] = binding.segmentation_id + + +class HyperVQuantumPlugin(db_base_plugin_v2.QuantumDbPluginV2, + l3_db.L3_NAT_db_mixin): + + # This attribute specifies whether the plugin supports or not + # bulk operations. Name mangling is used in order to ensure it + # is qualified by class + __native_bulk_support = True + supported_extension_aliases = ["provider", "router", "binding", "quotas"] + + network_view = "extension:provider_network:view" + network_set = "extension:provider_network:set" + binding_view = "extension:port_binding:view" + binding_set = "extension:port_binding:set" + + def __init__(self, configfile=None): + self._db = hyperv_db.HyperVPluginDB() + self._db.initialize() + + self._set_tenant_network_type() + + self._parse_network_vlan_ranges() + self._create_network_providers_map() + + self._db.sync_vlan_allocations(self._network_vlan_ranges) + + self._setup_rpc() + + def _set_tenant_network_type(self): + tenant_network_type = cfg.CONF.HYPERV.tenant_network_type + if tenant_network_type not in [constants.TYPE_LOCAL, + constants.TYPE_FLAT, + constants.TYPE_VLAN, + constants.TYPE_NONE]: + msg = _( + "Invalid tenant_network_type: %(tenant_network_type)s. " + "Agent terminated!") % locals() + raise q_exc.InvalidInput(error_message=msg) + self._tenant_network_type = tenant_network_type + + def _setup_rpc(self): + # RPC support + self.topic = topics.PLUGIN + self.conn = rpc.create_connection(new=True) + self.notifier = agent_notifier_api.AgentNotifierApi( + topics.AGENT) + self.callbacks = rpc_callbacks.HyperVRpcCallbacks(self.notifier) + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, + fanout=False) + # Consume from all consumers in a thread + self.conn.consume_in_thread() + + def _check_view_auth(self, context, resource, action): + return policy.check(context, action, resource) + + def _enforce_set_auth(self, context, resource, action): + policy.enforce(context, action, resource) + + def _parse_network_vlan_ranges(self): + self._network_vlan_ranges = {} + for entry in cfg.CONF.HYPERV.network_vlan_ranges: + entry = entry.strip() + if ':' in entry: + try: + physical_network, vlan_min, vlan_max = entry.split(':') + self._add_network_vlan_range(physical_network.strip(), + int(vlan_min), + int(vlan_max)) + except ValueError as ex: + msg = _( + "Invalid network VLAN range: " + "'%(range)s' - %(e)s. Agent terminated!"), \ + {'range': entry, 'e': ex} + raise q_exc.InvalidInput(error_message=msg) + else: + self._add_network(entry) + LOG.info(_("Network VLAN ranges: %s"), self._network_vlan_ranges) + + def _add_network_vlan_range(self, physical_network, vlan_min, vlan_max): + self._add_network(physical_network) + self._network_vlan_ranges[physical_network].append( + (vlan_min, vlan_max)) + + def _add_network(self, physical_network): + if physical_network not in self._network_vlan_ranges: + self._network_vlan_ranges[physical_network] = [] + + def _check_vlan_id_in_range(self, physical_network, vlan_id): + for r in self._network_vlan_ranges[physical_network]: + if vlan_id >= r[0] and vlan_id <= r[1]: + return True + return False + + def _create_network_providers_map(self): + self._network_providers_map = { + constants.TYPE_LOCAL: LocalNetworkProvider(), + constants.TYPE_FLAT: FlatNetworkProvider(), + constants.TYPE_VLAN: VlanNetworkProvider() + } + + def _process_provider_create(self, context, session, attrs): + network_type = attrs.get(provider.NETWORK_TYPE) + network_type_set = attributes.is_attr_set(network_type) + if not network_type_set: + if self._tenant_network_type == constants.TYPE_NONE: + raise q_exc.TenantNetworksDisabled() + network_type = self._tenant_network_type + attrs[provider.NETWORK_TYPE] = network_type + + if network_type not in self._network_providers_map: + msg = _("Network type %s not supported") % network_type + raise q_exc.InvalidInput(error_message=msg) + p = self._network_providers_map[network_type] + # Provider specific network creation + p.create_network(session, attrs) + + if network_type_set: + self._enforce_set_auth(context, attrs, self.network_set) + + def create_network(self, context, network): + session = context.session + with session.begin(subtransactions=True): + network_attrs = network['network'] + self._process_provider_create(context, session, network_attrs) + + net = super(HyperVQuantumPlugin, self).create_network( + context, network) + + network_type = network_attrs[provider.NETWORK_TYPE] + physical_network = network_attrs[provider.PHYSICAL_NETWORK] + segmentation_id = network_attrs[provider.SEGMENTATION_ID] + + self._db.add_network_binding( + session, net['id'], network_type, + physical_network, segmentation_id) + + self._process_l3_create(context, network['network'], net['id']) + self._extend_network_dict_provider(context, net) + self._extend_network_dict_l3(context, net) + + LOG.debug(_("Created network: %s"), net['id']) + return net + + def _extend_network_dict_provider(self, context, network): + if self._check_view_auth(context, network, self.network_view): + binding = self._db.get_network_binding( + context.session, network['id']) + network[provider.NETWORK_TYPE] = binding.network_type + p = self._network_providers_map[binding.network_type] + p.extend_network_dict(network, binding) + + def _check_provider_update(self, context, attrs): + network_type = attrs.get(provider.NETWORK_TYPE) + physical_network = attrs.get(provider.PHYSICAL_NETWORK) + segmentation_id = attrs.get(provider.SEGMENTATION_ID) + + network_type_set = attributes.is_attr_set(network_type) + physical_network_set = attributes.is_attr_set(physical_network) + segmentation_id_set = attributes.is_attr_set(segmentation_id) + + if not (network_type_set or physical_network_set or + segmentation_id_set): + return + + msg = _("plugin does not support updating provider attributes") + raise q_exc.InvalidInput(error_message=msg) + + def update_network(self, context, id, network): + network_attrs = network['network'] + self._check_provider_update(context, network_attrs) + # Authorize before exposing plugin details to client + self._enforce_set_auth(context, network_attrs, self.network_set) + + session = context.session + with session.begin(subtransactions=True): + net = super(HyperVQuantumPlugin, self).update_network(context, id, + network) + self._process_l3_update(context, network['network'], id) + self._extend_network_dict_provider(context, net) + self._extend_network_dict_l3(context, net) + return net + + def delete_network(self, context, id): + session = context.session + with session.begin(subtransactions=True): + binding = self._db.get_network_binding(session, id) + super(HyperVQuantumPlugin, self).delete_network(context, id) + p = self._network_providers_map[binding.network_type] + p.delete_network(session, binding) + # the network_binding record is deleted via cascade from + # the network record, so explicit removal is not necessary + self.notifier.network_delete(context, id) + + def get_network(self, context, id, fields=None): + net = super(HyperVQuantumPlugin, self).get_network(context, id, None) + self._extend_network_dict_provider(context, net) + self._extend_network_dict_l3(context, net) + return self._fields(net, fields) + + def get_networks(self, context, filters=None, fields=None): + nets = super(HyperVQuantumPlugin, self).get_networks( + context, filters, None) + for net in nets: + self._extend_network_dict_provider(context, net) + self._extend_network_dict_l3(context, net) + + # TODO(rkukura): Filter on extended provider attributes. + nets = self._filter_nets_l3(context, nets, filters) + return [self._fields(net, fields) for net in nets] + + def _extend_port_dict_binding(self, context, port): + if self._check_view_auth(context, port, self.binding_view): + port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_HYPERV + return port + + def create_port(self, context, port): + port = super(HyperVQuantumPlugin, self).create_port(context, port) + return self._extend_port_dict_binding(context, port) + + def get_port(self, context, id, fields=None): + port = super(HyperVQuantumPlugin, self).get_port(context, id, fields) + return self._fields(self._extend_port_dict_binding(context, port), + fields) + + def get_ports(self, context, filters=None, fields=None): + ports = super(HyperVQuantumPlugin, self).get_ports( + context, filters, fields) + return [self._fields(self._extend_port_dict_binding(context, port), + fields) for port in ports] + + def update_port(self, context, id, port): + original_port = super(HyperVQuantumPlugin, self).get_port( + context, id) + port = super(HyperVQuantumPlugin, self).update_port(context, id, port) + if original_port['admin_state_up'] != port['admin_state_up']: + binding = self._db.get_network_binding( + None, port['network_id']) + self.notifier.port_update(context, port, + binding.network_type, + binding.segmentation_id, + binding.physical_network) + return self._extend_port_dict_binding(context, port) + + def delete_port(self, context, id, l3_port_check=True): + # if needed, check to see if this is a port owned by + # and l3-router. If so, we should prevent deletion. + if l3_port_check: + self.prevent_l3_port_deletion(context, id) + self.disassociate_floatingips(context, id) + + super(HyperVQuantumPlugin, self).delete_port(context, id) + self.notifier.port_delete(context, id) diff --git a/quantum/plugins/hyperv/model.py b/quantum/plugins/hyperv/model.py new file mode 100644 index 0000000000..57b4ff9391 --- /dev/null +++ b/quantum/plugins/hyperv/model.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String + +from quantum.db.models_v2 import model_base + + +class VlanAllocation(model_base.BASEV2): + """Represents allocation state of vlan_id on physical network""" + __tablename__ = 'hyperv_vlan_allocations' + + physical_network = Column(String(64), nullable=False, primary_key=True) + vlan_id = Column(Integer, nullable=False, primary_key=True, + autoincrement=False) + allocated = Column(Boolean, nullable=False) + + def __init__(self, physical_network, vlan_id): + self.physical_network = physical_network + self.vlan_id = vlan_id + self.allocated = False + + +class NetworkBinding(model_base.BASEV2): + """Represents binding of virtual network to physical realization""" + __tablename__ = 'hyperv_network_bindings' + + network_id = Column(String(36), + ForeignKey('networks.id', ondelete="CASCADE"), + primary_key=True) + network_type = Column(String(32), nullable=False) + physical_network = Column(String(64)) + segmentation_id = Column(Integer) + + def __init__(self, network_id, network_type, physical_network, + segmentation_id): + self.network_id = network_id + self.network_type = network_type + self.physical_network = physical_network + self.segmentation_id = segmentation_id diff --git a/quantum/plugins/hyperv/rpc_callbacks.py b/quantum/plugins/hyperv/rpc_callbacks.py new file mode 100644 index 0000000000..9425c049af --- /dev/null +++ b/quantum/plugins/hyperv/rpc_callbacks.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# @author: Alessandro Pilotti, Cloudbase Solutions Srl + +import sys + +from quantum.api.v2 import attributes +from quantum.common import constants as q_const +from quantum.common import exceptions as q_exc +from quantum.common import rpc as q_rpc +from quantum.common import topics +from quantum.db import db_base_plugin_v2 +from quantum.db import dhcp_rpc_base +from quantum.db import l3_db +from quantum.db import l3_rpc_base +from quantum.extensions import portbindings +from quantum.extensions import providernet as provider +from quantum.openstack.common import cfg +from quantum.openstack.common import log as logging +from quantum.openstack.common import rpc +from quantum.openstack.common.rpc import proxy +from quantum.plugins.hyperv import db as hyperv_db +from quantum.plugins.hyperv.common import constants +from quantum import policy + +LOG = logging.getLogger(__name__) + + +class HyperVRpcCallbacks( + dhcp_rpc_base.DhcpRpcCallbackMixin, + l3_rpc_base.L3RpcCallbackMixin): + + # Set RPC API version to 1.0 by default. + RPC_API_VERSION = '1.0' + + def __init__(self, notifier): + self.notifier = notifier + self._db = hyperv_db.HyperVPluginDB() + + def create_rpc_dispatcher(self): + '''Get the rpc dispatcher for this manager. + + If a manager would like to set an rpc API version, or support more than + one class as the target of rpc messages, override this method. + ''' + return q_rpc.PluginRpcDispatcher([self]) + + def get_device_details(self, rpc_context, **kwargs): + """Agent requests device details""" + agent_id = kwargs.get('agent_id') + device = kwargs.get('device') + LOG.debug(_("Device %(device)s details requested from %(agent_id)s"), + locals()) + port = self._db.get_port(device) + if port: + binding = self._db.get_network_binding(None, port['network_id']) + entry = {'device': device, + 'network_id': port['network_id'], + 'port_id': port['id'], + 'admin_state_up': port['admin_state_up'], + 'network_type': binding.network_type, + 'segmentation_id': binding.segmentation_id, + 'physical_network': binding.physical_network} + # Set the port status to UP + self._db.set_port_status(port['id'], q_const.PORT_STATUS_ACTIVE) + else: + entry = {'device': device} + LOG.debug(_("%s can not be found in database"), device) + return entry + + def update_device_down(self, rpc_context, **kwargs): + """Device no longer exists on agent""" + # (TODO) garyk - live migration and port status + agent_id = kwargs.get('agent_id') + device = kwargs.get('device') + LOG.debug(_("Device %(device)s no longer exists on %(agent_id)s"), + locals()) + port = self._db.get_port(device) + if port: + entry = {'device': device, + 'exists': True} + # Set port status to DOWN + self._db.set_port_status(port['id'], q_const.PORT_STATUS_DOWN) + else: + entry = {'device': device, + 'exists': False} + LOG.debug(_("%s can not be found in database"), device) + return entry diff --git a/quantum/tests/unit/hyperv/__init__.py b/quantum/tests/unit/hyperv/__init__.py new file mode 100644 index 0000000000..c5618535cf --- /dev/null +++ b/quantum/tests/unit/hyperv/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py b/quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py new file mode 100644 index 0000000000..ffddbea28c --- /dev/null +++ b/quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py @@ -0,0 +1,113 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Cloudbase Solutions SRL +# Copyright 2013 Pedro Navarro Perez +# 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 virtual switch quantum driver +""" + +import mock +import sys + +import unittest2 as unittest + +from quantum.openstack.common import cfg +from quantum.plugins.hyperv.agent import hyperv_quantum_agent + + +class TestHyperVQuantumAgent(unittest.TestCase): + + def setUp(self): + self.addCleanup(cfg.CONF.reset) + # Avoid rpc initialization for unit tests + cfg.CONF.set_override('rpc_backend', + 'quantum.openstack.common.rpc.impl_fake') + self.agent = hyperv_quantum_agent.HyperVQuantumAgent() + self.agent.plugin_rpc = mock.Mock() + self.agent.context = mock.Mock() + self.agent.agent_id = mock.Mock() + self.agent._utils = mock.Mock() + + def tearDown(self): + cfg.CONF.reset() + + def test_port_bound(self): + port = mock.Mock() + net_uuid = 'my-net-uuid' + with mock.patch.object( + self.agent._utils, 'connect_vnic_to_vswitch'): + with mock.patch.object( + self.agent._utils, 'set_vswitch_port_vlan_id'): + self.agent._port_bound(port, net_uuid, 'vlan', None, None) + + def test_port_unbound(self): + map = { + 'network_type': 'vlan', + 'vswitch_name': 'fake-vswitch', + 'ports': [], + 'vlan_id': 1} + net_uuid = 'my-net-uuid' + network_vswitch_map = (net_uuid, map) + with mock.patch.object(self.agent, + '_get_network_vswitch_map_by_port_id', + return_value=network_vswitch_map): + with mock.patch.object( + self.agent._utils, + 'disconnect_switch_port'): + self.agent._port_unbound(net_uuid) + + def test_treat_devices_added_returns_true_for_missing_device(self): + attrs = {'get_device_details.side_effect': Exception()} + self.agent.plugin_rpc.configure_mock(**attrs) + self.assertTrue(self.agent._treat_devices_added([{}])) + + def mock_treat_devices_added(self, details, func_name): + """ + :param details: the details to return for the device + :param func_name: the function that should be called + :returns: whether the named function was called + """ + attrs = {'get_device_details.return_value': details} + self.agent.plugin_rpc.configure_mock(**attrs) + with mock.patch.object(self.agent, func_name) as func: + self.assertFalse(self.agent._treat_devices_added([{}])) + return func.called + + def test_treat_devices_added_updates_known_port(self): + details = mock.MagicMock() + details.__contains__.side_effect = lambda x: True + self.assertTrue(self.mock_treat_devices_added(details, + '_treat_vif_port')) + + def test_treat_devices_removed_returns_true_for_missing_device(self): + attrs = {'update_device_down.side_effect': Exception()} + self.agent.plugin_rpc.configure_mock(**attrs) + self.assertTrue(self.agent._treat_devices_removed([{}])) + + def mock_treat_devices_removed(self, port_exists): + details = dict(exists=port_exists) + attrs = {'update_device_down.return_value': details} + self.agent.plugin_rpc.configure_mock(**attrs) + with mock.patch.object(self.agent, '_port_unbound') as func: + self.assertFalse(self.agent._treat_devices_removed([{}])) + self.assertEqual(func.called, not port_exists) + + def test_treat_devices_removed_unbinds_port(self): + self.mock_treat_devices_removed(False) + + def test_treat_devices_removed_ignores_missing_port(self): + self.mock_treat_devices_removed(False) diff --git a/quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py b/quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py new file mode 100644 index 0000000000..afcaf0e2ad --- /dev/null +++ b/quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Cloudbase Solutions SRL +# Copyright 2013 Pedro Navarro Perez +# 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 contextlib + +from quantum import context +from quantum.extensions import portbindings +from quantum.manager import QuantumManager +from quantum.openstack.common import cfg +from quantum.tests.unit import test_db_plugin as test_plugin + + +class HyperVQuantumPluginTestCase(test_plugin.QuantumDbPluginV2TestCase): + + _plugin_name = ('quantum.plugins.hyperv.' + 'hyperv_quantum_plugin.HyperVQuantumPlugin') + + def setUp(self): + super(HyperVQuantumPluginTestCase, self).setUp(self._plugin_name) + + +class TestHyperVVirtualSwitchBasicGet( + test_plugin.TestBasicGet, HyperVQuantumPluginTestCase): + pass + + +class TestHyperVVirtualSwitchV2HTTPResponse( + test_plugin.TestV2HTTPResponse, HyperVQuantumPluginTestCase): + pass + + +class TestHyperVVirtualSwitchPortsV2( + test_plugin.TestPortsV2, HyperVQuantumPluginTestCase): + def test_port_vif_details(self): + plugin = QuantumManager.get_plugin() + with self.port(name='name') as port: + port_id = port['port']['id'] + self.assertEqual(port['port']['binding:vif_type'], + portbindings.VIF_TYPE_HYPERV) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + non_admin_port = plugin.get_port(ctx, port_id) + self.assertTrue('status' in non_admin_port) + self.assertFalse('binding:vif_type' in non_admin_port) + + def test_ports_vif_details(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + plugin = QuantumManager.get_plugin() + with contextlib.nested(self.port(), self.port()) as (port1, port2): + ctx = context.get_admin_context() + ports = plugin.get_ports(ctx) + self.assertEqual(len(ports), 2) + for port in ports: + self.assertEqual(port['binding:vif_type'], + portbindings.VIF_TYPE_HYPERV) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + ports = plugin.get_ports(ctx) + self.assertEqual(len(ports), 2) + for non_admin_port in ports: + self.assertTrue('status' in non_admin_port) + self.assertFalse('binding:vif_type' in non_admin_port) + + +class TestHyperVVirtualSwitchNetworksV2( + test_plugin.TestNetworksV2, HyperVQuantumPluginTestCase): + pass diff --git a/quantum/tests/unit/hyperv/test_hyperv_rpcapi.py b/quantum/tests/unit/hyperv/test_hyperv_rpcapi.py new file mode 100644 index 0000000000..f20fdf2f2e --- /dev/null +++ b/quantum/tests/unit/hyperv/test_hyperv_rpcapi.py @@ -0,0 +1,126 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Cloudbase Solutions SRL +# Copyright 2013 Pedro Navarro Perez +# 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 hyperv quantum rpc +""" + +import mock +import unittest2 + +from quantum.agent import rpc as agent_rpc +from quantum.common import topics +from quantum.openstack.common import context +from quantum.openstack.common import rpc +from quantum.plugins.hyperv.common import constants +from quantum.plugins.hyperv import agent_notifier_api as ana + + +class rpcHyperVApiTestCase(unittest2.TestCase): + + def _test_hyperv_quantum_api( + self, rpcapi, topic, method, rpc_method, **kwargs): + ctxt = context.RequestContext('fake_user', 'fake_project') + expected_retval = 'foo' if method == 'call' else None + expected_msg = rpcapi.make_msg(method, **kwargs) + expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION + if rpc_method == 'cast' and method == 'run_instance': + kwargs['call'] = False + + rpc_method_mock = mock.Mock() + rpc_method_mock.return_value = expected_retval + setattr(rpc, rpc_method, rpc_method_mock) + + retval = getattr(rpcapi, method)(ctxt, **kwargs) + + self.assertEqual(retval, expected_retval) + + expected_args = [ctxt, topic, expected_msg] + for arg, expected_arg in zip(rpc_method_mock.call_args[0], + expected_args): + self.assertEqual(arg, expected_arg) + + def test_delete_network(self): + rpcapi = ana.AgentNotifierApi(topics.AGENT) + self._test_hyperv_quantum_api( + rpcapi, + topics.get_topic_name( + topics.AGENT, + topics.NETWORK, + topics.DELETE), + 'network_delete', rpc_method='fanout_cast', + network_id='fake_request_spec') + + def test_port_update(self): + rpcapi = ana.AgentNotifierApi(topics.AGENT) + self._test_hyperv_quantum_api( + rpcapi, + topics.get_topic_name( + topics.AGENT, + topics.PORT, + topics.UPDATE), + 'port_update', rpc_method='fanout_cast', + port='fake_port', + network_type='fake_network_type', + segmentation_id='fake_segmentation_id', + physical_network='fake_physical_network') + + def test_port_delete(self): + rpcapi = ana.AgentNotifierApi(topics.AGENT) + self._test_hyperv_quantum_api( + rpcapi, + topics.get_topic_name( + topics.AGENT, + topics.PORT, + topics.DELETE), + 'port_delete', rpc_method='fanout_cast', + port_id='port_id') + + def test_tunnel_update(self): + rpcapi = ana.AgentNotifierApi(topics.AGENT) + self._test_hyperv_quantum_api( + rpcapi, + topics.get_topic_name( + topics.AGENT, + constants.TUNNEL, + topics.UPDATE), + 'tunnel_update', rpc_method='fanout_cast', + tunnel_ip='fake_ip', tunnel_id='fake_id') + + def test_device_details(self): + rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + self._test_hyperv_quantum_api( + rpcapi, topics.PLUGIN, + 'get_device_details', rpc_method='call', + device='fake_device', + agent_id='fake_agent_id') + + def test_update_device_down(self): + rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + self._test_hyperv_quantum_api( + rpcapi, topics.PLUGIN, + 'update_device_down', rpc_method='call', + device='fake_device', + agent_id='fake_agent_id') + + def test_tunnel_sync(self): + rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + self._test_hyperv_quantum_api( + rpcapi, topics.PLUGIN, + 'tunnel_sync', rpc_method='call', + tunnel_ip='fake_tunnel_ip')