vmware-nsx/vmware_nsx/plugins/nsx_v3/utils.py
Anna Khmelnitsky 5f3153e47f NSXv3: Move away from locking in cert provider
Since client certificate provider may be used both before and after
fork, refcount state is not necessarily zero at fork time (same
problem with refork). This commit replaces refcount-based model with
simple file-per-thread approach. Each thread will create and delete
its own client certificate file.

Change-Id: Idd14295b527c068f4b798ea96f8a53f61f9a18db
2017-07-13 08:55:22 -07:00

161 lines
6.3 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 threading
from oslo_config import cfg
from oslo_log import log as logging
from neutron import version as n_version
from neutron_lib import context as q_context
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.plugins.nsx_v3 import cert_utils
from vmware_nsxlib import v3
from vmware_nsxlib.v3 import client_cert
from vmware_nsxlib.v3 import config
NSX_NEUTRON_PLUGIN = 'NSX Neutron plugin'
OS_NEUTRON_ID_SCOPE = 'os-neutron-id'
LOG = logging.getLogger(__name__)
class DbCertProvider(client_cert.ClientCertProvider):
"""Write cert data from DB to file and delete after use
New file with random filename is created for each thread. This
is not most efficient, but the safest way to avoid race conditions,
since backend connections can occur both before and after neutron
fork.
"""
EXPIRATION_ALERT_DAYS = 30 # days prior to expiration
_thread_local = threading.local()
def __init__(self):
# Note: we cannot initialize filename here, because this call
# happens before neutron fork, meaning variable initialized here
# will be shared between all neutron processes (which will cause file
# collisions).
# The file can be shared between different connections within same
# process, if they happen to do the SSL handshake simultaneously.
# Such collisions are handled with refcount and locking.
super(DbCertProvider, self).__init__(None)
random.seed()
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):
# No certificate file available - need to create one
# Choose a random filename to contain the certificate
self._thread_local._filename = '/tmp/.' + str(
random.randint(1, 10000000))
try:
context = q_context.get_admin_context()
db_storage_driver = cert_utils.DbCertificateStorageDriver(
context)
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._thread_local._filename
if not os.path.exists(os.path.dirname(filename)):
if len(os.path.dirname(filename)) > 0:
os.makedirs(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
LOG.debug("Prepared client certificate file")
return self
def _on_exit(self):
if os.path.isfile(self._thread_local._filename):
os.remove(self._thread_local._filename)
LOG.debug("Deleted client certificate file")
self._thread_local._filename = None
def __exit__(self, type, value, traceback):
self._on_exit()
def filename(self):
return self._thread_local._filename
def get_client_cert_provider():
if not cfg.CONF.nsx_v3.nsx_use_client_auth:
return None
if cfg.CONF.nsx_v3.nsx_client_cert_storage.lower() == 'none':
# Admin is responsible for providing cert file, the plugin
# should not touch it
return client_cert.ClientCertProvider(
cfg.CONF.nsx_v3.nsx_client_cert_file)
if cfg.CONF.nsx_v3.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.
# Pid is appended to avoid file collisions between neutron servers
return DbCertProvider()
def get_nsxlib_wrapper(nsx_username=None, nsx_password=None, basic_auth=False):
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()
nsxlib_config = config.NsxLibConfig(
username=nsx_username or cfg.CONF.nsx_v3.nsx_api_user,
password=nsx_password or cfg.CONF.nsx_v3.nsx_api_password,
client_cert_provider=client_cert_provider,
retries=cfg.CONF.nsx_v3.http_retries,
insecure=cfg.CONF.nsx_v3.insecure,
ca_file=cfg.CONF.nsx_v3.ca_file,
concurrent_connections=cfg.CONF.nsx_v3.concurrent_connections,
http_timeout=cfg.CONF.nsx_v3.http_timeout,
http_read_timeout=cfg.CONF.nsx_v3.http_read_timeout,
conn_idle_timeout=cfg.CONF.nsx_v3.conn_idle_timeout,
http_provider=None,
max_attempts=cfg.CONF.nsx_v3.retries,
nsx_api_managers=cfg.CONF.nsx_v3.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)
return v3.NsxLib(nsxlib_config)