diff --git a/devstack/lib/vmware_nsx_v3 b/devstack/lib/vmware_nsx_v3 index 5911359c67..0dd7c854f5 100644 --- a/devstack/lib/vmware_nsx_v3 +++ b/devstack/lib/vmware_nsx_v3 @@ -40,6 +40,9 @@ METADATA_PROXY_SHARED_SECRET=${METADATA_PROXY_SHARED_SECRET:-} NSX_XTRACE=$(set +o | grep xtrace) set +o xtrace +# File to store client certificate and PK +CLIENT_CERT_FILE=${DEST}/data/neutron/client.pem + source $TOP_DIR/lib/neutron_plugins/ovs_base @@ -179,6 +182,11 @@ function neutron_plugin_configure_service { _nsxv3_ini_set metadata_proxy $METADATA_PROXY_UUID iniset $NEUTRON_CONF DEFAULT dhcp_agent_notification False fi + if [[ "$NSX_USE_CLIENT_CERT_AUTH" == "True" ]]; then + _nsxv3_ini_set nsx_use_client_auth "True" + _nsxv3_ini_set nsx_client_cert_file "$CLIENT_CERT_FILE" + _nsxv3_ini_set nsx_client_cert_storage "nsx-db" + fi } function neutron_plugin_setup_interface_driver { @@ -203,6 +211,10 @@ function init_vmware_nsx_v3 { die $LINENO "Native support does not require DHCP and Metadata agents!" fi fi + # Generate client certificate + if [[ "$NSX_USE_CLIENT_CERT_AUTH" == "True" ]]; then + nsxadmin -o generate -r certificate + fi if ! is_set NSX_GATEWAY_NETWORK_INTERFACE; then echo "NSX_GATEWAY_NETWORK_INTERFACE not set not configuring routes" return @@ -257,6 +269,9 @@ function stop_vmware_nsx_v3 { for address in $addresses; do sudo ip addr add dev $NSX_GATEWAY_NETWORK_INTERFACE $address done + + # Clean client certificate if exists + nsxadmin -o clean -r certificate } # Restore xtrace diff --git a/devstack/tools/nsxv3_cleanup.py b/devstack/tools/nsxv3_cleanup.py index 7f4b435fa1..6fbeee7c81 100755 --- a/devstack/tools/nsxv3_cleanup.py +++ b/devstack/tools/nsxv3_cleanup.py @@ -128,6 +128,9 @@ class NSXClient(object): headers['Authorization'] = "Basic %s" % auth headers['Content-Type'] = content_type headers['Accept'] = accept_type + # allow admin user to delete entities created + # under openstack principal identity + headers['X-Allow-Overwrite'] = "True" self.headers = headers def get(self, endpoint=None, params=None): diff --git a/doc/source/admin_util.rst b/doc/source/admin_util.rst index c4caedc4db..11ccf3f5d8 100644 --- a/doc/source/admin_util.rst +++ b/doc/source/admin_util.rst @@ -294,6 +294,21 @@ Orphaned DHCP Servers nsxadmin -r orphaned-dhcp-servers -o nsx-clean +Client Certificate +~~~~~~~~~~~~~~~~~~ + +- Generate new client certificate (this command will delete previous certificate if exists):: + + nsxadmin -r certificate -o generate --property username= --property password= + +- Delete client certificate:: + + nsxadmin -r certificate -o clean + +- Show client certificate details:: + + nsxadmin -r certificate -o show + Upgrade Steps (Version 1.0.0 to Version 1.1.0) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index c17918f902..c27843a79f 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -272,6 +272,17 @@ nsx_v3_opts = [ "[://][:]\nIf scheme is not " "provided https is used. If port is not provided port " "80 is used for http and port 443 for https.")), + cfg.BoolOpt('nsx_use_client_auth', + default=False, + help=_("Use client certificate in NSX manager " + "authentication")), + cfg.StrOpt('nsx_client_cert_file', + default='', + help=_("File to contain client certificate and private key")), + cfg.StrOpt('nsx_client_cert_storage', + default='nsx-db', + choices=['nsx-db', 'none'], + help=_("Storage type for client certificate sensitive data")), cfg.StrOpt('default_overlay_tz', deprecated_name='default_overlay_tz_uuid', help=_("This is the name or UUID of the default NSX overlay " diff --git a/vmware_nsx/common/exceptions.py b/vmware_nsx/common/exceptions.py index 7554ecdbd9..297ee689ed 100644 --- a/vmware_nsx/common/exceptions.py +++ b/vmware_nsx/common/exceptions.py @@ -23,6 +23,10 @@ class NsxPluginException(n_exc.NeutronException): message = _("An unexpected error occurred in the NSX Plugin: %(err_msg)s") +class ClientCertificateException(NsxPluginException): + message = _("Client certificate error: %(err_msg)s") + + class InvalidVersion(NsxPluginException): message = _("Unable to fulfill request with version %(version)s.") diff --git a/vmware_nsx/db/db.py b/vmware_nsx/db/db.py index a4a550b2b9..c3f72cccda 100644 --- a/vmware_nsx/db/db.py +++ b/vmware_nsx/db/db.py @@ -448,3 +448,27 @@ def del_nsx_ipam_subnet_pool(session, subnet_id, nsx_pool_id): return (session.query(nsx_models.NsxSubnetIpam). filter_by(subnet_id=subnet_id, nsx_pool_id=nsx_pool_id).delete()) + + +def get_certificate(session, purpose): + try: + cert_entry = session.query( + nsx_models.NsxCertificateRepository).filter_by( + purpose=purpose).one() + return cert_entry.certificate, cert_entry.private_key + except exc.NoResultFound: + return None, None + + +def save_certificate(session, purpose, cert, pk): + with session.begin(subtransactions=True): + cert_entry = nsx_models.NsxCertificateRepository( + purpose=purpose, + certificate=cert, + private_key=pk) + session.add(cert_entry) + + +def delete_certificate(session, purpose): + return (session.query(nsx_models.NsxCertificateRepository). + filter_by(purpose=purpose).delete()) diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index 9b24ac69e4..76017b3931 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -e816d4fe9d4f +dd9fe5a3a526 diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/dd9fe5a3a526_nsx_add_certificate_table.py b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/dd9fe5a3a526_nsx_add_certificate_table.py new file mode 100644 index 0000000000..1dc7f1c636 --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/dd9fe5a3a526_nsx_add_certificate_table.py @@ -0,0 +1,39 @@ +# Copyright 2016 VMware, Inc. +# +# 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. + +"""NSX Adds certificate table for client certificate management + +Revision ID: dd9fe5a3a526 +Revises: e816d4fe9d4f +Create Date: 2017-01-06 12:30:01.070022 + +""" + +# revision identifiers, used by Alembic. +revision = 'dd9fe5a3a526' +down_revision = 'e816d4fe9d4f' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + + op.create_table('nsx_certificates', + sa.Column('purpose', sa.String(length=32), nullable=False), + sa.Column('certificate', sa.String(length=9216), nullable=False), + sa.Column('private_key', sa.String(length=5120), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('purpose')) diff --git a/vmware_nsx/db/nsx_models.py b/vmware_nsx/db/nsx_models.py index 0a88c7c8cc..873804abaf 100644 --- a/vmware_nsx/db/nsx_models.py +++ b/vmware_nsx/db/nsx_models.py @@ -377,3 +377,16 @@ class NsxSubnetIpam(model_base.BASEV2, models.TimestampMixin): # before the pool does subnet_id = sa.Column(sa.String(36), primary_key=True) nsx_pool_id = sa.Column(sa.String(36), primary_key=True) + + +class NsxCertificateRepository(model_base.BASEV2, models.TimestampMixin): + """Stores certificate and private key per logical purpose. + + For now, will have zero or one rows with nsxv3 client certificate + """ + __tablename__ = 'nsx_certificates' + purpose = sa.Column(sa.String(32), + nullable=False, + primary_key=True) + certificate = sa.Column(sa.String(9216), nullable=False) + private_key = sa.Column(sa.String(5120), nullable=False) diff --git a/vmware_nsx/plugins/nsx_v3/cert_utils.py b/vmware_nsx/plugins/nsx_v3/cert_utils.py new file mode 100644 index 0000000000..752f6f4f41 --- /dev/null +++ b/vmware_nsx/plugins/nsx_v3/cert_utils.py @@ -0,0 +1,53 @@ +# 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. + +from vmware_nsx.db import db as nsx_db + + +NSX_OPENSTACK_IDENTITY = "com.vmware.nsx.openstack" + + +class DbCertificateStorageDriver(object): + """Storage for certificate and private key in neutron DB""" + # TODO(annak): Add private key encryption + def __init__(self, context): + self._context = context + + def store_cert(self, purpose, certificate, private_key): + nsx_db.save_certificate(self._context.session, purpose, + certificate, private_key) + + def get_cert(self, purpose): + return nsx_db.get_certificate(self._context.session, purpose) + + def delete_cert(self, purpose): + return nsx_db.delete_certificate(self._context.session, purpose) + + +class DummyCertificateStorageDriver(object): + """Dummy driver API implementation + + Used for external certificate import scenario + (nsx_client_cert_storage == None) + """ + + def store_cert(self, purpose, certificate, private_key): + pass + + def get_cert(self, purpose): + pass + + def delete_cert(self, purpose): + pass diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 824caeb623..e84bdedf9a 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -91,10 +91,12 @@ from vmware_nsx.extensions import advancedserviceproviders as as_providers from vmware_nsx.extensions import maclearning as mac_ext from vmware_nsx.extensions import providersecuritygroup as provider_sg from vmware_nsx.extensions import securitygrouplogging as sg_logging +from vmware_nsx.plugins.nsx_v3 import cert_utils from vmware_nsx.plugins.nsx_v3 import utils as v3_utils from vmware_nsx.services.qos.common import utils as qos_com_utils from vmware_nsx.services.qos.nsx_v3 import utils as qos_utils from vmware_nsx.services.trunk.nsx_v3 import driver as trunk_driver +from vmware_nsxlib.v3 import client_cert from vmware_nsxlib.v3 import exceptions as nsx_lib_exc from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts from vmware_nsxlib.v3 import resources as nsx_resources @@ -176,6 +178,10 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._extension_manager.initialize() self.supported_extension_aliases.extend( self._extension_manager.extension_aliases()) + + if cfg.CONF.nsx_v3.nsx_use_client_auth: + self._init_client_certificate() + self.nsxlib = v3_utils.get_nsxlib_wrapper() # reinitialize the cluster upon fork for api workers to ensure each # process has its own keepalive loops + state @@ -241,6 +247,29 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( attributes.SUBNETS, ['_ext_extend_subnet_dict']) + def _init_client_certificate(self): + """Load certificate data from storage""" + + LOG.info(_LI("NSX authenication will use client certificate " + "with storage type %s"), + cfg.CONF.nsx_v3.nsx_client_cert_storage) + if cfg.CONF.nsx_v3.nsx_client_cert_storage.lower() == 'none': + # nothing to do - admin is responsible for storing cert file + # in the filesystem of each neutron host + return + + if cfg.CONF.nsx_v3.nsx_client_cert_storage.lower() == 'nsx-db': + context = q_context.get_admin_context() + db_storage_driver = cert_utils.DbCertificateStorageDriver( + context) + cert_manager = client_cert.ClientCertificateManager( + cert_utils.NSX_OPENSTACK_IDENTITY, None, db_storage_driver) + if not cert_manager.exists(): + msg = _("Unable to load from nsx-db") + raise nsx_exc.ClientCertificateException(err_msg=msg) + # TODO(annak): add certificate expiration warning if expires soon + cert_manager.export_pem(cfg.CONF.nsx_v3.nsx_client_cert_file) + def _init_nsx_profiles(self): LOG.debug("Initializing NSX v3 port spoofguard switching profile") if not self._init_port_security_profile(): diff --git a/vmware_nsx/plugins/nsx_v3/utils.py b/vmware_nsx/plugins/nsx_v3/utils.py index 4d21e2fb1d..dab68a43b4 100644 --- a/vmware_nsx/plugins/nsx_v3/utils.py +++ b/vmware_nsx/plugins/nsx_v3/utils.py @@ -19,15 +19,20 @@ from neutron import version as n_version from vmware_nsxlib import v3 from vmware_nsxlib.v3 import config - NSX_NEUTRON_PLUGIN = 'NSX Neutron plugin' OS_NEUTRON_ID_SCOPE = 'os-neutron-id' -def get_nsxlib_wrapper(): +def get_nsxlib_wrapper(nsx_username=None, nsx_password=None, basic_auth=False): + client_cert_file = None + if not basic_auth and cfg.CONF.nsx_v3.nsx_use_client_auth: + # if basic auth requested, dont use cert file even if provided + client_cert_file = cfg.CONF.nsx_v3.nsx_client_cert_file + nsxlib_config = config.NsxLibConfig( - username=cfg.CONF.nsx_v3.nsx_api_user, - password=cfg.CONF.nsx_v3.nsx_api_password, + username=nsx_username or cfg.CONF.nsx_v3.nsx_api_user, + password=nsx_password or cfg.CONF.nsx_v3.nsx_api_password, + client_cert_file=client_cert_file, retries=cfg.CONF.nsx_v3.http_retries, insecure=cfg.CONF.nsx_v3.insecure, ca_file=cfg.CONF.nsx_v3.ca_file, diff --git a/vmware_nsx/shell/admin/plugins/common/constants.py b/vmware_nsx/shell/admin/plugins/common/constants.py index 915997c94e..882b1ef63a 100644 --- a/vmware_nsx/shell/admin/plugins/common/constants.py +++ b/vmware_nsx/shell/admin/plugins/common/constants.py @@ -32,6 +32,7 @@ SECURITY_GROUPS = 'security-groups' PORTS = 'ports' METADATA_PROXY = 'metadata-proxy' ORPHANED_DHCP_SERVERS = 'orphaned-dhcp-servers' +CERTIFICATE = 'certificate' # NSXV Resource Constants EDGES = 'edges' diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/certificates.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/certificates.py new file mode 100644 index 0000000000..28643fc71e --- /dev/null +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/certificates.py @@ -0,0 +1,120 @@ +# 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 logging + +from vmware_nsx._i18n import _LI +from vmware_nsx.plugins.nsx_v3 import cert_utils +from vmware_nsx.shell.admin.plugins.common import constants +from vmware_nsx.shell.admin.plugins.common import utils as admin_utils +from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils +from vmware_nsx.shell import resources as shell +from vmware_nsxlib.v3 import client_cert +from vmware_nsxlib.v3 import trust_management + +from neutron.callbacks import registry +from neutron import context +from oslo_config import cfg + +LOG = logging.getLogger(__name__) + +# default certificate validity period in days (10 years) +DEFAULT_CERT_VALIDITY_PERIOD = 3650 + + +def get_certificate_manager(**kwargs): + username, password = None, None + if kwargs.get('property'): + properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) + username = properties.get('user') + password = properties.get('password') + + storage_driver_type = cfg.CONF.nsx_v3.nsx_client_cert_storage.lower() + LOG.info(_LI("Certificate storage is %s"), storage_driver_type) + if storage_driver_type == 'nsx-db': + storage_driver = cert_utils.DbCertificateStorageDriver( + context.get_admin_context()) + elif storage_driver_type == 'none': + storage_driver = cert_utils.DummyCertificateStorageDriver() + # TODO(annak) - add support for barbican storage driver + + nsx_client = utils.get_nsxv3_client(username, password, True) + nsx_trust = trust_management.NsxLibTrustManagement(nsx_client, {}) + + return client_cert.ClientCertificateManager( + cert_utils.NSX_OPENSTACK_IDENTITY, + nsx_trust, + storage_driver) + + +@admin_utils.output_header +def generate_cert(resource, event, trigger, **kwargs): + """Generate self signed client certificate and private key + """ + + cert_manager = get_certificate_manager(**kwargs) + if cert_manager.exists(): + # Need to delete cert first + cert_manager.delete() + + cert_manager.generate(subject={}, + valid_for_days=DEFAULT_CERT_VALIDITY_PERIOD) + + +@admin_utils.output_header +def delete_cert(resource, event, trigger, **kwargs): + """Delete client certificate and private key """ + + cert_manager = get_certificate_manager(**kwargs) + if cert_manager.exists(): + cert_manager.delete() + + +@admin_utils.output_header +def show_cert(resource, event, trigger, **kwargs): + """Show client certificate details """ + + cert_manager = get_certificate_manager(**kwargs) + if cert_manager.exists(): + cert_pem, key_pem = cert_manager.get_pem() + expires_on = cert_manager.expires_on() + expires_in_days = cert_manager.expires_in_days() + if expires_in_days > 0: + LOG.info(_LI("Client certificate is valid. " + "Expires on %(date)s (in %(days)d days)"), + {'date': expires_on, 'days': expires_in_days}) + + else: + LOG.info(_LI("Client certificate expired on %s."), expires_on) + + LOG.info(cert_pem) + # TODO(annak): show certificate details such as subject and crypto + # and add verification same certificate is registered in NSX. + # For imported certificate, fetch from NSX + else: + LOG.info(_LI("Client certificate was not registered in the system")) + + +registry.subscribe(generate_cert, + constants.CERTIFICATE, + shell.Operations.GENERATE.value) + +registry.subscribe(show_cert, + constants.CERTIFICATE, + shell.Operations.SHOW.value) + +registry.subscribe(delete_cert, + constants.CERTIFICATE, + shell.Operations.CLEAN.value) diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py index 80a530b7b6..6f2f1bf93f 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/utils.py @@ -24,12 +24,23 @@ from vmware_nsxlib.v3 import nsx_constants _NSXLIB = None -def get_nsxv3_client(): - return get_connected_nsxlib().client +def get_nsxv3_client(nsx_username=None, nsx_password=None, + use_basic_auth=False): + + return get_connected_nsxlib(nsx_username, + nsx_password, + use_basic_auth).client -def get_connected_nsxlib(): +def get_connected_nsxlib(nsx_username=None, nsx_password=None, + use_basic_auth=False): global _NSXLIB + + # for non-default agruments, initiate new lib + if nsx_username or use_basic_auth: + return v3_utils.get_nsxlib_wrapper(nsx_username, + nsx_password, + use_basic_auth) if _NSXLIB is None: _NSXLIB = v3_utils.get_nsxlib_wrapper() return _NSXLIB diff --git a/vmware_nsx/shell/resources.py b/vmware_nsx/shell/resources.py index 5ad207e7de..5ae954f857 100644 --- a/vmware_nsx/shell/resources.py +++ b/vmware_nsx/shell/resources.py @@ -52,6 +52,9 @@ class Operations(enum.Enum): NSX_MIGRATE_V_V3 = 'nsx-migrate-v-v3' MIGRATE_TO_POLICY = 'migrate-to-policy' STATUS = 'status' + GENERATE = 'generate' + IMPORT = 'import' + SHOW = 'show' ops = [op.value for op in Operations] @@ -91,6 +94,11 @@ nsxv3_resources = { constants.ORPHANED_DHCP_SERVERS: Resource(constants.ORPHANED_DHCP_SERVERS, [Operations.NSX_LIST.value, Operations.NSX_CLEAN.value]), + constants.CERTIFICATE: Resource(constants.CERTIFICATE, + [Operations.GENERATE.value, + Operations.SHOW.value, + Operations.CLEAN.value, + Operations.IMPORT.value]) } # Add supported NSX-V resources in this dictionary diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index 19b66c3ff5..9d57d54e05 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import mock import six from webob import exc @@ -34,6 +36,7 @@ from neutron.tests.unit.extensions \ import test_l3_ext_gw_mode as test_ext_gw_mode from neutron.tests.unit.scheduler \ import test_dhcp_agent_scheduler as test_dhcpagent +from neutron.tests.unit import testlib_api from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet @@ -43,6 +46,7 @@ from neutron_lib.plugins import directory from oslo_config import cfg from oslo_utils import uuidutils +from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import utils from vmware_nsx.plugins.nsx_v3 import plugin as nsx_plugin from vmware_nsx.tests import unit as vmware @@ -98,6 +102,10 @@ def _mock_nsx_backend_calls(): "vmware_nsxlib.v3.NsxLibBridgeCluster.get_id_by_name_or_id", return_value=uuidutils.generate_uuid()).start() + mock.patch( + "vmware_nsxlib.v3.NsxLibTransportZone.get_id_by_name_or_id", + return_value=uuidutils.generate_uuid()).start() + mock.patch( "vmware_nsxlib.v3.NsxLibBridgeEndpoint.create", side_effect=_return_id_key).start() @@ -134,6 +142,10 @@ def _mock_nsx_backend_calls(): "vmware_nsxlib.v3.resources.LogicalDhcpServer.create_binding", side_effect=_return_id_key).start() + mock.patch( + "vmware_nsxlib.v3.NsxLib.get_version", + return_value="0.6.0").start() + class NsxV3PluginTestCaseMixin(test_plugin.NeutronDbPluginV2TestCase, nsxlib_testcase.NsxClientTestCase): @@ -705,3 +717,100 @@ class ExtGwModeTestCase(test_ext_gw_mode.ExtGwModeIntTestCase, L3NatTest): def test_router_gateway_set_fail_after_port_create(self): self.skipTest("TBD") + + +class NsxV3PluginClientCertTestCase(testlib_api.WebTestCase): + + CERT = "-----BEGIN CERTIFICATE-----\n" \ + "MIIDJTCCAg0CBFh36j0wDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCVVMxEzAR\n" \ + "BgNVBAgMCkNhbGlmb3JuaWExDjAMBgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZNeVVu\n" \ + "aXQxEjAQBgNVBAMMCW15b3JnLmNvbTAeFw0xNzAxMTIyMDQyMzdaFw0yNzAxMTAy\n" \ + "MDQyMzdaMFcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQ4wDAYD\n" \ + "VQQKDAVNeU9yZzEPMA0GA1UECwwGTXlVbml0MRIwEAYDVQQDDAlteW9yZy5jb20w\n" \ + "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/wsYintlWVaSeXwaSrdPa\n" \ + "+AHtL1ooH7q0uf6tt+6Rwiy10YRjAVJhapj9995gqgJ2402J+3gzNXLCbXjjDR/D\n" \ + "9xjAzKHu61r0AVNd9/0+8yXQrEDuzlwHSCKz+zjq5ZEZ7RkLIUdreaZJFPTCwry3\n" \ + "wuTnBfqcE7xWl6WfWR8evooV+ZzIfjQdoSliIyn3YGxNN5pc1P40qt0pxOsNBGXG\n" \ + "2FIZXpML8TpKw0ga/wE70CJd6tRvSsAADxQXehfKvGtHvlJYS+3cTahC7reQXJnc\n" \ + "qsjgYkiWyhhR4jdcTD/tDlVcJroM1jFVxpsCg/AU3srWWWeAGyVe42ZhqWVf0Urz\n" \ + "AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA/lLfmXe8wPyBhN/VMb5bu5Ey56qz+j\n" \ + "jCn7tz7FjRvsB9P0fLUDOBKNwyon3yopDNYJ4hnm4yKoHCHURQLZKWHzm0XKzE+4\n" \ + "cA/M13M8OEg5otnVVHhz1FPQWnJq7bLHh/KXYcc5Rkc7UeHEPj0sDjfUjCPGdepc\n" \ + "Ghu1ZcgHsL4JCuvcadG+RFGeDTug3yO92Fj2uFy5DlzzWOZSi4otpZRd9JZkAtZ1\n" \ + "umZRBJ2A504nJx4MplmNqvLNkmxMLKQdvZYNNiYr6icOavDOJA5RhzgoppJZkV2w\n" \ + "v2oC+8BFarXnZSk37HAWjwcaqzBLbIyPYpClW5IYMr8LiixSBACc+4w=\n" \ + "-----END CERTIFICATE-----\n" + + PKEY = "-----BEGIN PRIVATE KEY-----\n" \ + "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC/wsYintlWVaSe\n" \ + "XwaSrdPa+AHtL1ooH7q0uf6tt+6Rwiy10YRjAVJhapj9995gqgJ2402J+3gzNXLC\n" \ + "bXjjDR/D9xjAzKHu61r0AVNd9/0+8yXQrEDuzlwHSCKz+zjq5ZEZ7RkLIUdreaZJ\n" \ + "FPTCwry3wuTnBfqcE7xWl6WfWR8evooV+ZzIfjQdoSliIyn3YGxNN5pc1P40qt0p\n" \ + "xOsNBGXG2FIZXpML8TpKw0ga/wE70CJd6tRvSsAADxQXehfKvGtHvlJYS+3cTahC\n" \ + "7reQXJncqsjgYkiWyhhR4jdcTD/tDlVcJroM1jFVxpsCg/AU3srWWWeAGyVe42Zh\n" \ + "qWVf0UrzAgMBAAECggEBAJrGuie9cQy3KZzOdD614RaPMPbhTnKuUYOH0GEk4YFy\n" \ + "aaYDS0iiC30njf8HLs10y3JsOuyRNU6X6F24AGe68xW3/pm3UUjHXG0wGLry68wA\n" \ + "c1g/gFV/6FXUSnZc4m7uBjUX4yvRm5TK5oV8TaZZifsEar9xWvrZDx4RXpQEWhL0\n" \ + "L/TyrOZSfRtBgdWX6Ag4XQVsCfZxJoCi2ZyvaMBsWTH06x9AGo1Io5t1AmA9Hsfb\n" \ + "6BsSz186nqb0fq4UMfrWrSCz7M/1s03+hBOVICH2TdaRDZLtDVa1b2x4sFpfdp9t\n" \ + "VVxuSHxcmvzOPMIv3NXwj0VitTYYJDBFKoEfx1mzhNkCgYEA59gYyBfpsuCOevP2\n" \ + "tn7IeysbtaoKDzHE+ksjs3sAn6Vr2Y0Lbed26NpdIVL6u3HAteJxqrIh0zpkpAtp\n" \ + "akdqlj86oRaBUqLXxK3QNpUx19f7KN7UsVAbzUJSOm2n1piPg261ktfhtms2rxnQ\n" \ + "+9yluINu+z1wS4FG9SwrRmwwfsUCgYEA072Ma1sj2MER5tmQw1zLANkzP1PAkUdy\n" \ + "+oDuJmU9A3/+YSIkm8dGprFglPkLUaf1B15oN6wCJVMpB1lza3PM/YT70rpqc7cq\n" \ + "PHJXQlZFMBhyVfIkCv3wICTLD5phhgAWlzlwm094f2uAnbG6WUkrVfZajuh0pW53\n" \ + "1i0OTfxAvlcCgYEAkDB2oSM2JhjApDlMbA2HtAqIbkA1h2OlpSDMMFjEd4WTALdW\n" \ + "r2CwNHtyRkJsS92gQ750gPvOS6daZifuxLlr0cu7M+piPbmnRdvvzbKWUC40NyP2\n" \ + "1dwDnnGr4EjIhI9XWh+lb5EyAJjHZrlAnxOIQawEft6kE2FwdxSkSWUJ+B0CgYEA\n" \ + "n2xYDXzRwKGdmPK2zGFRd5IRw9yLYNcq+vGYXdBb4Aa+wOO0LJYd2+Qxk/jvTMvo\n" \ + "8WNjlIcuFmxGuAHhpUXLUhaOhFtXS0jdxCVTDd9muI+vhoaKHLyVz53kRhs20m2+\n" \ + "lJ3q6wUq9MU8UX8/j3pH5rFV/cOIEAbcs6W4337OQIECgYEAoLtQyqXjH45FlCQx\n" \ + "xK8dY+GuxIP+TIwiq23yhu3e+3LIgXJw8DwBFN5yJyH2HMnhGkD4PurEx2sGHeLO\n" \ + "EG6L8PNDOxpvSzcgxwmZsUK6j3nAbKycF3PDDXA4kt8WDXBr86OMQsFtpjeO+fGh\n" \ + "YWJa+OKc2ExdeMewe9gKIDQ5stw=\n" \ + "-----END PRIVATE KEY-----\n" + + CERTFILE = '/tmp/client_cert.pem' + + def _init_config(self): + cfg.CONF.set_override('default_overlay_tz', NSX_TZ_NAME, 'nsx_v3') + cfg.CONF.set_override('native_dhcp_metadata', False, 'nsx_v3') + cfg.CONF.set_override('dhcp_profile', + NSX_DHCP_PROFILE_ID, 'nsx_v3') + cfg.CONF.set_override('metadata_proxy', + NSX_METADATA_PROXY_ID, 'nsx_v3') + + cfg.CONF.set_override('nsx_use_client_auth', True, 'nsx_v3') + cfg.CONF.set_override('nsx_client_cert_file', self.CERTFILE, 'nsx_v3') + cfg.CONF.set_override('nsx_client_cert_storage', 'nsx-db', 'nsx_v3') + + def _init_plugin(self): + self._tenant_id = test_plugin.TEST_TENANT_ID + self._init_config() + self.setup_coreplugin(PLUGIN_NAME, load_plugins=True) + + def test_init_without_cert(self): + # certificate not generated - exception should be raised + self.assertRaises(nsx_exc.ClientCertificateException, + self._init_plugin) + + def test_init_with_cert(self): + mock.patch( + "vmware_nsx.db.db.get_certificate", + return_value=(self.CERT, self.PKEY)).start() + + _mock_nsx_backend_calls() + self._init_plugin() + + # verify cert data was exported to CERTFILE + expected = self.CERT + self.PKEY + with open(self.CERTFILE, 'r') as f: + actual = f.read() + + self.assertEqual(expected, actual) + + # delete CERTFILE + os.remove(self.CERTFILE) + + # TODO(annak): add test that verifies bad crypto data raises exception + # when OPENSSL exception wrapper is available from NSXLIB