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
This commit is contained in:
Alessandro Pilotti 2013-01-04 20:32:09 +02:00
parent 90747ff037
commit 2d9e479eb3
17 changed files with 2005 additions and 0 deletions

View File

@ -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
# <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

View File

@ -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 = {

View File

@ -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.

View File

@ -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.

View File

@ -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 <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)

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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'

View File

@ -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)

View File

@ -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 <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)

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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')