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_802_QBG = '802.1qbg'
|
||||
VIF_TYPE_802_QBH = '802.1qbh'
|
||||
VIF_TYPE_HYPERV = 'hyperv'
|
||||
VIF_TYPE_OTHER = 'other'
|
||||
|
||||
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