vmware-nsx/vmware_nsx/plugins/nsx_v3/utils.py

569 lines
23 KiB
Python

# Copyright 2016 VMware, Inc.
# 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 os
import random
import re
from oslo_config import cfg
from oslo_context import context as context_utils
from oslo_log import log as logging
from oslo_utils import fileutils
from sqlalchemy.orm import exc
from neutron.db.models import securitygroup
from neutron import version as n_version
from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef
from neutron_lib import constants as const
from neutron_lib import context as q_context
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.db import db as nsx_db
from vmware_nsx.db import nsx_models
from vmware_nsx.plugins.nsx_v3 import cert_utils
from vmware_nsx.services.qos.common import utils as qos_utils
from vmware_nsxlib import v3
from vmware_nsxlib.v3 import client_cert
from vmware_nsxlib.v3 import config
from vmware_nsxlib.v3 import core_resources
from vmware_nsxlib.v3 import exceptions as nsxlib_exc
from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3 import policy
NSX_NEUTRON_PLUGIN = 'NSX Neutron plugin'
OS_NEUTRON_ID_SCOPE = 'os-neutron-id'
NSX_V3_PSEC_PROFILE_NAME = 'neutron_port_spoof_guard_profile'
NSX_V3_DHCP_PROFILE_NAME = 'neutron_port_dhcp_profile'
PORT_ERROR_TYPE_MISSING = "Missing port"
PORT_ERROR_TYPE_PROFILE = "Wrong switching profiles"
PORT_ERROR_TYPE_BINDINGS = "Wrong address binding"
# Default UUID for the global OS rule
NSX_V3_OS_DFW_UUID = '00000000-def0-0000-0fed-000000000000'
LOG = logging.getLogger(__name__)
def get_DbCertProvider(conf_path):
class DbCertProvider(client_cert.ClientCertProvider):
"""Write cert data from DB to file and delete after use
New provider object with random filename is created for each
request.
This is not most efficient, but the safest way to avoid race
conditions, since backend connections can occur both before and
after neutron fork, and several concurrent requests can occupy the
same thread.
Note that new cert filename for each request does not result in new
connection for each request (at least for now..)
"""
EXPIRATION_ALERT_DAYS = 30 # days prior to expiration
def __init__(self):
super(DbCertProvider, self).__init__(None)
random.seed()
self._filename = '/tmp/.' + str(random.randint(1, 10000000))
self.conf_path = conf_path
def _check_expiration(self, expires_in_days):
if expires_in_days > self.EXPIRATION_ALERT_DAYS:
return
if expires_in_days < 0:
LOG.error("Client certificate has expired %d days ago.",
expires_in_days * -1)
else:
LOG.warning("Client certificate expires in %d days. "
"Once expired, service will become unavailable.",
expires_in_days)
def __enter__(self):
try:
context = q_context.get_admin_context()
db_storage_driver = cert_utils.DbCertificateStorageDriver(
context, self.conf_path.nsx_client_cert_pk_password)
with client_cert.ClientCertificateManager(
cert_utils.NSX_OPENSTACK_IDENTITY,
None,
db_storage_driver) as cert_manager:
if not cert_manager.exists():
msg = _("Unable to load from nsx-db")
raise nsx_exc.ClientCertificateException(err_msg=msg)
filename = self._filename
if not os.path.exists(os.path.dirname(filename)):
if len(os.path.dirname(filename)) > 0:
fileutils.ensure_tree(os.path.dirname(filename))
cert_manager.export_pem(filename)
expires_in_days = cert_manager.expires_in_days()
self._check_expiration(expires_in_days)
except Exception as e:
self._on_exit()
raise e
return self
def _on_exit(self):
if os.path.isfile(self._filename):
os.remove(self._filename)
self._filename = None
def __exit__(self, type, value, traceback):
self._on_exit()
def filename(self):
return self._filename
return DbCertProvider
def get_client_cert_provider(conf_path=cfg.CONF.nsx_v3):
if not conf_path.nsx_use_client_auth:
return None
if conf_path.nsx_client_cert_storage.lower() == 'none':
# Admin is responsible for providing cert file, the plugin
# should not touch it
return client_cert.ClientCertProvider(
conf_path.nsx_client_cert_file)
if conf_path.nsx_client_cert_storage.lower() == 'nsx-db':
# Cert data is stored in DB, and written to file system only
# when new connection is opened, and deleted immediately after.
return get_DbCertProvider(conf_path)
def get_nsxlib_wrapper(nsx_username=None, nsx_password=None, basic_auth=False,
plugin_conf=None, allow_overwrite_header=False):
if not plugin_conf:
plugin_conf = cfg.CONF.nsx_v3
client_cert_provider = None
if not basic_auth:
# if basic auth requested, dont use cert file even if provided
client_cert_provider = get_client_cert_provider(conf_path=plugin_conf)
nsxlib_config = config.NsxLibConfig(
username=nsx_username or plugin_conf.nsx_api_user,
password=nsx_password or plugin_conf.nsx_api_password,
client_cert_provider=client_cert_provider,
retries=plugin_conf.http_retries,
insecure=plugin_conf.insecure,
ca_file=plugin_conf.ca_file,
concurrent_connections=plugin_conf.concurrent_connections,
http_timeout=plugin_conf.http_timeout,
http_read_timeout=plugin_conf.http_read_timeout,
conn_idle_timeout=plugin_conf.conn_idle_timeout,
http_provider=None,
max_attempts=plugin_conf.retries,
nsx_api_managers=plugin_conf.nsx_api_managers,
plugin_scope=OS_NEUTRON_ID_SCOPE,
plugin_tag=NSX_NEUTRON_PLUGIN,
plugin_ver=n_version.version_info.release_string(),
dns_nameservers=cfg.CONF.nsx_v3.nameservers,
dns_domain=cfg.CONF.nsx_v3.dns_domain,
allow_overwrite_header=allow_overwrite_header)
return v3.NsxLib(nsxlib_config)
def get_nsxpolicy_wrapper(nsx_username=None, nsx_password=None,
basic_auth=False, conf_path=None):
if not conf_path:
conf_path = cfg.CONF.nsx_p
client_cert_provider = None
if not basic_auth:
# if basic auth requested, dont use cert file even if provided
client_cert_provider = get_client_cert_provider(
conf_path=conf_path)
nsxlib_config = config.NsxLibConfig(
username=nsx_username or conf_path.nsx_api_user,
password=nsx_password or conf_path.nsx_api_password,
client_cert_provider=client_cert_provider,
retries=conf_path.http_retries,
insecure=conf_path.insecure,
ca_file=conf_path.ca_file,
concurrent_connections=conf_path.concurrent_connections,
http_timeout=conf_path.http_timeout,
http_read_timeout=conf_path.http_read_timeout,
conn_idle_timeout=conf_path.conn_idle_timeout,
http_provider=None,
max_attempts=conf_path.retries,
nsx_api_managers=conf_path.nsx_api_managers,
plugin_scope=OS_NEUTRON_ID_SCOPE,
plugin_tag=NSX_NEUTRON_PLUGIN,
plugin_ver=n_version.version_info.release_string(),
dns_nameservers=conf_path.nameservers,
dns_domain=conf_path.dns_domain,
allow_passthrough=(conf_path.allow_passthrough
if hasattr(conf_path, 'allow_passthrough')
else False),
realization_max_attempts=(conf_path.realization_max_attempts
if hasattr(conf_path,
'realization_max_attempts')
else 50),
realization_wait_sec=(conf_path.realization_wait_sec
if hasattr(conf_path, 'realization_wait_sec')
else 1))
return policy.NsxPolicyLib(nsxlib_config)
def get_orphaned_dhcp_servers(context, plugin, nsxlib, dhcp_profile_uuid=None):
# An orphaned DHCP server means the associated neutron network
# does not exist or has no DHCP-enabled subnet.
orphaned_servers = []
server_net_pairs = []
# Find matching DHCP servers (for a given dhcp_profile_uuid).
response = nsxlib.dhcp_server.list()
for dhcp_server in response['results']:
if (dhcp_profile_uuid and
dhcp_server['dhcp_profile_id'] != dhcp_profile_uuid):
continue
found = False
neutron_obj = False
for tag in dhcp_server.get('tags', []):
if tag['scope'] == 'os-neutron-net-id':
dhcp_server['neutron_net_id'] = tag['tag']
server_net_pairs.append((dhcp_server, tag['tag']))
found = True
if tag['scope'] == 'os-api-version':
neutron_obj = True
if not found and neutron_obj:
# The associated neutron network is not defined.
dhcp_server['neutron_net_id'] = None
orphaned_servers.append(dhcp_server)
# Check if there is DHCP-enabled subnet in each network.
for dhcp_server, net_id in server_net_pairs:
try:
network = plugin.get_network(context, net_id)
except Exception:
# The associated neutron network is not found in DB.
orphaned_servers.append(dhcp_server)
continue
dhcp_enabled = False
for subnet_id in network['subnets']:
subnet = plugin.get_subnet(context, subnet_id)
if subnet['enable_dhcp']:
dhcp_enabled = True
break
if not dhcp_enabled:
orphaned_servers.append(dhcp_server)
return orphaned_servers
def delete_orphaned_dhcp_server(context, nsxlib, server):
# Delete an orphaned DHCP server:
# (1) delete the attached logical DHCP port,
# (2) delete the logical DHCP server,
# (3) clean corresponding neutron DB entry.
# Return True if it was deleted, or false + error if not
try:
response = nsxlib.logical_port.get_by_attachment('DHCP_SERVICE',
server['id'])
if response and response['result_count'] > 0:
nsxlib.logical_port.delete(response['results'][0]['id'])
nsxlib.dhcp_server.delete(server['id'])
net_id = server.get('neutron_net_id')
if net_id:
# Delete neutron_net_id -> dhcp_service_id mapping from the DB.
nsx_db.delete_neutron_nsx_service_binding(
context.session, net_id,
nsx_constants.SERVICE_DHCP)
return True, None
except Exception as e:
return False, e
def get_orphaned_networks(context, nsxlib):
nsx_switches = nsxlib.logical_switch.list()['results']
missing_networks = []
for nsx_switch in nsx_switches:
# check if it exists in the neutron DB
net_ids = nsx_db.get_net_ids(context.session, nsx_switch['id'])
if not net_ids:
# Skip non-neutron networks, by tags
neutron_net = False
for tag in nsx_switch.get('tags', []):
if tag.get('scope') == 'os-neutron-net-id':
neutron_net = True
nsx_switch['neutron_net_id'] = tag['tag']
break
if neutron_net:
missing_networks.append(nsx_switch)
return missing_networks
def get_orphaned_routers(context, nsxlib):
nsx_routers = nsxlib.logical_router.list()['results']
missing_routers = []
for nsx_router in nsx_routers:
# check if it exists in the neutron DB
neutron_id = nsx_db.get_neutron_from_nsx_router_id(context.session,
nsx_router['id'])
if not neutron_id:
# Skip non-neutron routers, by tags
for tag in nsx_router.get('tags', []):
if tag.get('scope') == 'os-neutron-router-id':
nsx_router['neutron_router_id'] = tag['tag']
missing_routers.append(nsx_router)
break
return missing_routers
def delete_orphaned_router(nsxlib, nsx_id):
# Delete an orphaned logical router from the NSX:
# (1) delete the attached ports,
# (2) delete the logical router
# Return True if it was deleted, or false + error if not
try:
# first delete its ports
ports = nsxlib.logical_router_port.get_by_router_id(nsx_id)
for port in ports:
nsxlib.logical_router_port.delete(port['id'])
nsxlib.logical_router.delete(nsx_id)
except Exception as e:
return False, e
else:
return True, None
def get_security_groups_mappings(context):
q = context.session.query(
securitygroup.SecurityGroup.name,
securitygroup.SecurityGroup.id,
nsx_models.NeutronNsxFirewallSectionMapping.nsx_id,
nsx_models.NeutronNsxSecurityGroupMapping.nsx_id).join(
nsx_models.NeutronNsxFirewallSectionMapping,
nsx_models.NeutronNsxSecurityGroupMapping).all()
sg_mappings = [{'name': mapp[0],
'id': mapp[1],
'section-id': mapp[2],
'nsx-securitygroup-id': mapp[3]}
for mapp in q]
return sg_mappings
def get_orphaned_firewall_sections(context, nsxlib):
fw_sections = nsxlib.firewall_section.list()
sg_mappings = get_security_groups_mappings(context)
orphaned_sections = []
for fw_section in fw_sections:
for sg_db in sg_mappings:
if fw_section['id'] == sg_db['section-id']:
break
else:
# Skip non-neutron sections, by tags
neutron_obj = False
for tag in fw_section.get('tags', []):
if tag['scope'] == 'os-api-version':
neutron_obj = True
if tag.get('scope') == 'os-neutron-secgr-id':
fw_section['neutron_sg_id'] = tag['tag']
if neutron_obj:
orphaned_sections.append(fw_section)
return orphaned_sections
def get_security_group_rules_mappings(context):
q = context.session.query(
securitygroup.SecurityGroupRule.id,
nsx_models.NeutronNsxRuleMapping.nsx_id).join(
nsx_models.NeutronNsxRuleMapping).all()
sg_mappings = [{'rule_id': mapp[0],
'nsx_rule_id': mapp[1]}
for mapp in q]
return sg_mappings
def get_orphaned_firewall_section_rules(context, nsxlib):
fw_sections = nsxlib.firewall_section.list()
sg_mappings = get_security_groups_mappings(context)
rules_mappings = get_security_group_rules_mappings(context)
orphaned_rules = []
nsx_rules_in_mappings = [r['nsx_rule_id'] for r in rules_mappings]
for fw_section in fw_sections:
for sg_db in sg_mappings:
if (fw_section['id'] == sg_db['section-id'] and
sg_db['id'] != NSX_V3_OS_DFW_UUID):
# found the right neutron SG
section_rules = nsxlib.firewall_section.get_rules(
fw_section['id'])['results']
for nsx_rule in section_rules:
if nsx_rule['id'] not in nsx_rules_in_mappings:
# orphaned rule
orphaned_rules.append(
{'security-group-name': sg_db['name'],
'security-group-id': sg_db['id'],
'section-id': fw_section['id'],
'rule-id': nsx_rule['id']})
return orphaned_rules
def get_dhcp_profile_id(nsxlib):
profiles = nsxlib.switching_profile.find_by_display_name(
NSX_V3_DHCP_PROFILE_NAME)
if profiles and len(profiles) == 1:
return profiles[0]['id']
LOG.warning("Could not find DHCP profile on backend")
def get_spoofguard_profile_id(nsxlib):
profiles = nsxlib.switching_profile.find_by_display_name(
NSX_V3_PSEC_PROFILE_NAME)
if profiles and len(profiles) == 1:
return profiles[0]['id']
LOG.warning("Could not find Spoof Guard profile on backend")
def add_profile_mismatch(problems, neutron_id, nsx_id, prf_id, title):
msg = ('Wrong %(title)s profile %(prf_id)s') % {'title': title,
'prf_id': prf_id}
problems.append({'neutron_id': neutron_id,
'nsx_id': nsx_id,
'error': msg,
'error_type': PORT_ERROR_TYPE_PROFILE})
def get_port_nsx_id(session, neutron_id):
# get the nsx port id from the DB mapping
try:
mapping = (session.query(nsx_models.NeutronNsxPortMapping).
filter_by(neutron_id=neutron_id).
one())
return mapping['nsx_port_id']
except exc.NoResultFound:
pass
def get_mismatch_logical_ports(context, nsxlib, plugin, get_filters=None):
neutron_ports = plugin.get_ports(context, filters=get_filters)
# get pre-defined profile ids
dhcp_profile_id = get_dhcp_profile_id(nsxlib)
dhcp_profile_key = (
core_resources.SwitchingProfileTypes.SWITCH_SECURITY)
spoofguard_profile_id = get_spoofguard_profile_id(nsxlib)
spoofguard_profile_key = (
core_resources.SwitchingProfileTypes.SPOOF_GUARD)
qos_profile_key = core_resources.SwitchingProfileTypes.QOS
problems = []
for port in neutron_ports:
neutron_id = port['id']
# get the network nsx id from the mapping table
nsx_id = get_port_nsx_id(context.session, neutron_id)
if not nsx_id:
# skip external ports
pass
else:
try:
nsx_port = nsxlib.logical_port.get(nsx_id)
except nsxlib_exc.ResourceNotFound:
problems.append({'neutron_id': neutron_id,
'nsx_id': nsx_id,
'error': 'Missing from backend',
'error_type': PORT_ERROR_TYPE_MISSING})
continue
# Port found on backend!
# Check that it has all the expected switch profiles.
# create a dictionary of the current profiles:
profiles_dict = {}
for prf in nsx_port['switching_profile_ids']:
profiles_dict[prf['key']] = prf['value']
# DHCP port: neutron dhcp profile should be attached
# to logical ports created for neutron DHCP but not
# for native DHCP.
if (port.get('device_owner') == const.DEVICE_OWNER_DHCP and
not cfg.CONF.nsx_v3.native_dhcp_metadata):
prf_id = profiles_dict[dhcp_profile_key]
if prf_id != dhcp_profile_id:
add_profile_mismatch(problems, neutron_id, nsx_id,
prf_id, "DHCP security")
# Port with QoS policy: a matching profile should be attached
qos_policy_id = qos_utils.get_port_policy_id(context,
neutron_id)
if qos_policy_id:
qos_profile_id = nsx_db.get_switch_profile_by_qos_policy(
context.session, qos_policy_id)
prf_id = profiles_dict[qos_profile_key]
if prf_id != qos_profile_id:
add_profile_mismatch(problems, neutron_id, nsx_id,
prf_id, "QoS")
# Port with security & fixed ips/address pairs:
# neutron spoofguard profile should be attached
port_sec, has_ip = plugin._determine_port_security_and_has_ip(
context, port)
addr_pair = port.get(addr_apidef.ADDRESS_PAIRS)
if port_sec and (has_ip or addr_pair):
prf_id = profiles_dict[spoofguard_profile_key]
if prf_id != spoofguard_profile_id:
add_profile_mismatch(problems, neutron_id, nsx_id,
prf_id, "Spoof Guard")
# Check the address bindings
if port_sec:
nsx_address_bindings = nsx_port.get('address_bindings', [])
nsx_ips = [x['ip_address'] for x in nsx_address_bindings]
nsx_macs = [x['mac_address'] for x in nsx_address_bindings]
neutron_ips = [x['ip_address']
for x in port.get('fixed_ips', [])]
neutron_mac = port['mac_address']
different_macs = [mac for mac in nsx_macs
if mac != neutron_mac]
if (len(nsx_ips) != len(neutron_ips) or
set(nsx_ips) != set(neutron_ips)):
problems.append({'neutron_id': neutron_id,
'nsx_id': nsx_id,
'port': port,
'error': 'Different IP address bindings',
'error_type': PORT_ERROR_TYPE_BINDINGS})
elif different_macs:
problems.append({'neutron_id': neutron_id,
'nsx_id': nsx_id,
'port': port,
'error': 'Different MAC address bindings',
'error_type': PORT_ERROR_TYPE_BINDINGS})
return problems
def inject_headers():
ctx = context_utils.get_current()
if ctx:
ctx_dict = ctx.to_dict()
# Remove unsupported characters from the user-id
user_id = ctx_dict.get('user_identity')
re.sub('[^A-Za-z0-9]+', '', user_id)
return {'X-NSX-EUSER': user_id,
'X-NSX-EREQID': ctx_dict.get('request_id')}
return {}
def inject_requestid_header():
ctx = context_utils.get_current()
if ctx:
return {'X-NSX-EREQID': ctx.__dict__.get('request_id')}
return {}