--- /dev/null
+[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
+# <physical_network>[:<vlan_min>:<vlan_max>] 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 <physical_network>:<vswitch>
+# 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
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 = {
--- /dev/null
+# 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.
--- /dev/null
+# 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.
--- /dev/null
+# 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 <physical_network>:<vswitch> '
+ '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)
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4\r
+\r
+# Copyright 2013 Cloudbase Solutions SRL\r
+# All Rights Reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
--- /dev/null
+# 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'
--- /dev/null
+# 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)
--- /dev/null
+# 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 <physical_network>:<vlan_min>:<vlan_max> "
+ "or <physical_network>")),
+]
+
+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)
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4\r
+\r
+# Copyright 2013 Cloudbase Solutions SRL\r
+# All Rights Reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4\r
+\r
+# Copyright 2013 Cloudbase Solutions SRL\r
+# Copyright 2013 Pedro Navarro Perez\r
+# All Rights Reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+\r
+"""\r
+Unit tests for Windows Hyper-V virtual switch quantum driver\r
+"""\r
+\r
+import mock\r
+import sys\r
+\r
+import unittest2 as unittest\r
+\r
+from quantum.openstack.common import cfg\r
+from quantum.plugins.hyperv.agent import hyperv_quantum_agent\r
+\r
+\r
+class TestHyperVQuantumAgent(unittest.TestCase):\r
+\r
+ def setUp(self):\r
+ self.addCleanup(cfg.CONF.reset)\r
+ # Avoid rpc initialization for unit tests\r
+ cfg.CONF.set_override('rpc_backend',\r
+ 'quantum.openstack.common.rpc.impl_fake')\r
+ self.agent = hyperv_quantum_agent.HyperVQuantumAgent()\r
+ self.agent.plugin_rpc = mock.Mock()\r
+ self.agent.context = mock.Mock()\r
+ self.agent.agent_id = mock.Mock()\r
+ self.agent._utils = mock.Mock()\r
+\r
+ def tearDown(self):\r
+ cfg.CONF.reset()\r
+\r
+ def test_port_bound(self):\r
+ port = mock.Mock()\r
+ net_uuid = 'my-net-uuid'\r
+ with mock.patch.object(\r
+ self.agent._utils, 'connect_vnic_to_vswitch'):\r
+ with mock.patch.object(\r
+ self.agent._utils, 'set_vswitch_port_vlan_id'):\r
+ self.agent._port_bound(port, net_uuid, 'vlan', None, None)\r
+\r
+ def test_port_unbound(self):\r
+ map = {\r
+ 'network_type': 'vlan',\r
+ 'vswitch_name': 'fake-vswitch',\r
+ 'ports': [],\r
+ 'vlan_id': 1}\r
+ net_uuid = 'my-net-uuid'\r
+ network_vswitch_map = (net_uuid, map)\r
+ with mock.patch.object(self.agent,\r
+ '_get_network_vswitch_map_by_port_id',\r
+ return_value=network_vswitch_map):\r
+ with mock.patch.object(\r
+ self.agent._utils,\r
+ 'disconnect_switch_port'):\r
+ self.agent._port_unbound(net_uuid)\r
+\r
+ def test_treat_devices_added_returns_true_for_missing_device(self):\r
+ attrs = {'get_device_details.side_effect': Exception()}\r
+ self.agent.plugin_rpc.configure_mock(**attrs)\r
+ self.assertTrue(self.agent._treat_devices_added([{}]))\r
+\r
+ def mock_treat_devices_added(self, details, func_name):\r
+ """\r
+ :param details: the details to return for the device\r
+ :param func_name: the function that should be called\r
+ :returns: whether the named function was called\r
+ """\r
+ attrs = {'get_device_details.return_value': details}\r
+ self.agent.plugin_rpc.configure_mock(**attrs)\r
+ with mock.patch.object(self.agent, func_name) as func:\r
+ self.assertFalse(self.agent._treat_devices_added([{}]))\r
+ return func.called\r
+\r
+ def test_treat_devices_added_updates_known_port(self):\r
+ details = mock.MagicMock()\r
+ details.__contains__.side_effect = lambda x: True\r
+ self.assertTrue(self.mock_treat_devices_added(details,\r
+ '_treat_vif_port'))\r
+\r
+ def test_treat_devices_removed_returns_true_for_missing_device(self):\r
+ attrs = {'update_device_down.side_effect': Exception()}\r
+ self.agent.plugin_rpc.configure_mock(**attrs)\r
+ self.assertTrue(self.agent._treat_devices_removed([{}]))\r
+\r
+ def mock_treat_devices_removed(self, port_exists):\r
+ details = dict(exists=port_exists)\r
+ attrs = {'update_device_down.return_value': details}\r
+ self.agent.plugin_rpc.configure_mock(**attrs)\r
+ with mock.patch.object(self.agent, '_port_unbound') as func:\r
+ self.assertFalse(self.agent._treat_devices_removed([{}]))\r
+ self.assertEqual(func.called, not port_exists)\r
+\r
+ def test_treat_devices_removed_unbinds_port(self):\r
+ self.mock_treat_devices_removed(False)\r
+\r
+ def test_treat_devices_removed_ignores_missing_port(self):\r
+ self.mock_treat_devices_removed(False)\r
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4\r
+\r
+# Copyright 2013 Cloudbase Solutions SRL\r
+# Copyright 2013 Pedro Navarro Perez\r
+# All Rights Reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+\r
+import contextlib\r
+\r
+from quantum import context\r
+from quantum.extensions import portbindings\r
+from quantum.manager import QuantumManager\r
+from quantum.openstack.common import cfg\r
+from quantum.tests.unit import test_db_plugin as test_plugin\r
+\r
+\r
+class HyperVQuantumPluginTestCase(test_plugin.QuantumDbPluginV2TestCase):\r
+\r
+ _plugin_name = ('quantum.plugins.hyperv.'\r
+ 'hyperv_quantum_plugin.HyperVQuantumPlugin')\r
+\r
+ def setUp(self):\r
+ super(HyperVQuantumPluginTestCase, self).setUp(self._plugin_name)\r
+\r
+\r
+class TestHyperVVirtualSwitchBasicGet(\r
+ test_plugin.TestBasicGet, HyperVQuantumPluginTestCase):\r
+ pass\r
+\r
+\r
+class TestHyperVVirtualSwitchV2HTTPResponse(\r
+ test_plugin.TestV2HTTPResponse, HyperVQuantumPluginTestCase):\r
+ pass\r
+\r
+\r
+class TestHyperVVirtualSwitchPortsV2(\r
+ test_plugin.TestPortsV2, HyperVQuantumPluginTestCase):\r
+ def test_port_vif_details(self):\r
+ plugin = QuantumManager.get_plugin()\r
+ with self.port(name='name') as port:\r
+ port_id = port['port']['id']\r
+ self.assertEqual(port['port']['binding:vif_type'],\r
+ portbindings.VIF_TYPE_HYPERV)\r
+ # By default user is admin - now test non admin user\r
+ ctx = context.Context(user_id=None,\r
+ tenant_id=self._tenant_id,\r
+ is_admin=False,\r
+ read_deleted="no")\r
+ non_admin_port = plugin.get_port(ctx, port_id)\r
+ self.assertTrue('status' in non_admin_port)\r
+ self.assertFalse('binding:vif_type' in non_admin_port)\r
+\r
+ def test_ports_vif_details(self):\r
+ cfg.CONF.set_default('allow_overlapping_ips', True)\r
+ plugin = QuantumManager.get_plugin()\r
+ with contextlib.nested(self.port(), self.port()) as (port1, port2):\r
+ ctx = context.get_admin_context()\r
+ ports = plugin.get_ports(ctx)\r
+ self.assertEqual(len(ports), 2)\r
+ for port in ports:\r
+ self.assertEqual(port['binding:vif_type'],\r
+ portbindings.VIF_TYPE_HYPERV)\r
+ # By default user is admin - now test non admin user\r
+ ctx = context.Context(user_id=None,\r
+ tenant_id=self._tenant_id,\r
+ is_admin=False,\r
+ read_deleted="no")\r
+ ports = plugin.get_ports(ctx)\r
+ self.assertEqual(len(ports), 2)\r
+ for non_admin_port in ports:\r
+ self.assertTrue('status' in non_admin_port)\r
+ self.assertFalse('binding:vif_type' in non_admin_port)\r
+\r
+\r
+class TestHyperVVirtualSwitchNetworksV2(\r
+ test_plugin.TestNetworksV2, HyperVQuantumPluginTestCase):\r
+ pass\r
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4\r
+\r
+# Copyright 2013 Cloudbase Solutions SRL\r
+# Copyright 2013 Pedro Navarro Perez\r
+# All Rights Reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+\r
+"""\r
+Unit Tests for hyperv quantum rpc\r
+"""\r
+\r
+import mock\r
+import unittest2\r
+\r
+from quantum.agent import rpc as agent_rpc\r
+from quantum.common import topics\r
+from quantum.openstack.common import context\r
+from quantum.openstack.common import rpc\r
+from quantum.plugins.hyperv.common import constants\r
+from quantum.plugins.hyperv import agent_notifier_api as ana\r
+\r
+\r
+class rpcHyperVApiTestCase(unittest2.TestCase):\r
+\r
+ def _test_hyperv_quantum_api(\r
+ self, rpcapi, topic, method, rpc_method, **kwargs):\r
+ ctxt = context.RequestContext('fake_user', 'fake_project')\r
+ expected_retval = 'foo' if method == 'call' else None\r
+ expected_msg = rpcapi.make_msg(method, **kwargs)\r
+ expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION\r
+ if rpc_method == 'cast' and method == 'run_instance':\r
+ kwargs['call'] = False\r
+\r
+ rpc_method_mock = mock.Mock()\r
+ rpc_method_mock.return_value = expected_retval\r
+ setattr(rpc, rpc_method, rpc_method_mock)\r
+\r
+ retval = getattr(rpcapi, method)(ctxt, **kwargs)\r
+\r
+ self.assertEqual(retval, expected_retval)\r
+\r
+ expected_args = [ctxt, topic, expected_msg]\r
+ for arg, expected_arg in zip(rpc_method_mock.call_args[0],\r
+ expected_args):\r
+ self.assertEqual(arg, expected_arg)\r
+\r
+ def test_delete_network(self):\r
+ rpcapi = ana.AgentNotifierApi(topics.AGENT)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi,\r
+ topics.get_topic_name(\r
+ topics.AGENT,\r
+ topics.NETWORK,\r
+ topics.DELETE),\r
+ 'network_delete', rpc_method='fanout_cast',\r
+ network_id='fake_request_spec')\r
+\r
+ def test_port_update(self):\r
+ rpcapi = ana.AgentNotifierApi(topics.AGENT)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi,\r
+ topics.get_topic_name(\r
+ topics.AGENT,\r
+ topics.PORT,\r
+ topics.UPDATE),\r
+ 'port_update', rpc_method='fanout_cast',\r
+ port='fake_port',\r
+ network_type='fake_network_type',\r
+ segmentation_id='fake_segmentation_id',\r
+ physical_network='fake_physical_network')\r
+\r
+ def test_port_delete(self):\r
+ rpcapi = ana.AgentNotifierApi(topics.AGENT)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi,\r
+ topics.get_topic_name(\r
+ topics.AGENT,\r
+ topics.PORT,\r
+ topics.DELETE),\r
+ 'port_delete', rpc_method='fanout_cast',\r
+ port_id='port_id')\r
+\r
+ def test_tunnel_update(self):\r
+ rpcapi = ana.AgentNotifierApi(topics.AGENT)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi,\r
+ topics.get_topic_name(\r
+ topics.AGENT,\r
+ constants.TUNNEL,\r
+ topics.UPDATE),\r
+ 'tunnel_update', rpc_method='fanout_cast',\r
+ tunnel_ip='fake_ip', tunnel_id='fake_id')\r
+\r
+ def test_device_details(self):\r
+ rpcapi = agent_rpc.PluginApi(topics.PLUGIN)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi, topics.PLUGIN,\r
+ 'get_device_details', rpc_method='call',\r
+ device='fake_device',\r
+ agent_id='fake_agent_id')\r
+\r
+ def test_update_device_down(self):\r
+ rpcapi = agent_rpc.PluginApi(topics.PLUGIN)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi, topics.PLUGIN,\r
+ 'update_device_down', rpc_method='call',\r
+ device='fake_device',\r
+ agent_id='fake_agent_id')\r
+\r
+ def test_tunnel_sync(self):\r
+ rpcapi = agent_rpc.PluginApi(topics.PLUGIN)\r
+ self._test_hyperv_quantum_api(\r
+ rpcapi, topics.PLUGIN,\r
+ 'tunnel_sync', rpc_method='call',\r
+ tunnel_ip='fake_tunnel_ip')\r