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:
parent
90747ff037
commit
2d9e479eb3
82
etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini
Normal file
82
etc/quantum/plugins/hyperv/hyperv_quantum_plugin.ini
Normal 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
|
@ -35,6 +35,7 @@ VIF_TYPE_OVS = 'ovs'
|
|||||||
VIF_TYPE_BRIDGE = 'bridge'
|
VIF_TYPE_BRIDGE = 'bridge'
|
||||||
VIF_TYPE_802_QBG = '802.1qbg'
|
VIF_TYPE_802_QBG = '802.1qbg'
|
||||||
VIF_TYPE_802_QBH = '802.1qbh'
|
VIF_TYPE_802_QBH = '802.1qbh'
|
||||||
|
VIF_TYPE_HYPERV = 'hyperv'
|
||||||
VIF_TYPE_OTHER = 'other'
|
VIF_TYPE_OTHER = 'other'
|
||||||
|
|
||||||
EXTENDED_ATTRIBUTES_2_0 = {
|
EXTENDED_ATTRIBUTES_2_0 = {
|
||||||
|
16
quantum/plugins/hyperv/__init__.py
Normal file
16
quantum/plugins/hyperv/__init__.py
Normal 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.
|
16
quantum/plugins/hyperv/agent/__init__.py
Normal file
16
quantum/plugins/hyperv/agent/__init__.py
Normal 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.
|
353
quantum/plugins/hyperv/agent/hyperv_quantum_agent.py
Normal file
353
quantum/plugins/hyperv/agent/hyperv_quantum_agent.py
Normal 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)
|
283
quantum/plugins/hyperv/agent/utils.py
Normal file
283
quantum/plugins/hyperv/agent/utils.py
Normal 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
|
93
quantum/plugins/hyperv/agent_notifier_api.py
Normal file
93
quantum/plugins/hyperv/agent_notifier_api.py
Normal 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)
|
16
quantum/plugins/hyperv/common/__init__.py
Normal file
16
quantum/plugins/hyperv/common/__init__.py
Normal 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.
|
32
quantum/plugins/hyperv/common/constants.py
Normal file
32
quantum/plugins/hyperv/common/constants.py
Normal 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'
|
215
quantum/plugins/hyperv/db.py
Normal file
215
quantum/plugins/hyperv/db.py
Normal 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)
|
398
quantum/plugins/hyperv/hyperv_quantum_plugin.py
Normal file
398
quantum/plugins/hyperv/hyperv_quantum_plugin.py
Normal 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)
|
55
quantum/plugins/hyperv/model.py
Normal file
55
quantum/plugins/hyperv/model.py
Normal 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
|
102
quantum/plugins/hyperv/rpc_callbacks.py
Normal file
102
quantum/plugins/hyperv/rpc_callbacks.py
Normal 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
|
16
quantum/tests/unit/hyperv/__init__.py
Normal file
16
quantum/tests/unit/hyperv/__init__.py
Normal 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.
|
113
quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py
Normal file
113
quantum/tests/unit/hyperv/test_hyperv_quantum_agent.py
Normal 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)
|
88
quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py
Normal file
88
quantum/tests/unit/hyperv/test_hyperv_quantum_plugin.py
Normal 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
|
126
quantum/tests/unit/hyperv/test_hyperv_rpcapi.py
Normal file
126
quantum/tests/unit/hyperv/test_hyperv_rpcapi.py
Normal 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')
|
Loading…
Reference in New Issue
Block a user