From e6c1c1645795277f946929148f65c1e9257fd862 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Tue, 17 May 2016 13:59:07 +0300 Subject: [PATCH] Create common neutron module Move _build_client logic to ironic.common.neutron module. In future module will contain common functions to Neutron. Change-Id: I7b344d71d0f9ae34f7423099631bd25b5c5359bd --- etc/ironic/ironic.conf.sample | 103 ++++++++++++++------- ironic/common/neutron.py | 79 ++++++++++++++++ ironic/conf/opts.py | 4 +- ironic/dhcp/neutron.py | 77 ++-------------- ironic/tests/unit/common/test_neutron.py | 109 +++++++++++++++++++++++ ironic/tests/unit/dhcp/test_neutron.py | 69 -------------- 6 files changed, 270 insertions(+), 171 deletions(-) create mode 100644 ironic/common/neutron.py create mode 100644 ironic/tests/unit/common/test_neutron.py diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 797238952e..7f62ff60bd 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -234,11 +234,6 @@ # Allowed values: redis, dummy #rpc_zmq_matchmaker = redis -# Type of concurrency used. Either "native" or "eventlet" -# (string value) -# Allowed values: eventlet, native -#rpc_zmq_concurrency = eventlet - # Number of ZeroMQ contexts, defaults to 1. (integer value) #rpc_zmq_contexts = 1 @@ -268,13 +263,17 @@ # Expiration timeout in seconds of a name service record about # existing target ( < 0 means no timeout). (integer value) -#zmq_target_expire = 120 +#zmq_target_expire = 300 + +# Update period in seconds of a name service record about +# existing target. (integer value) +#zmq_target_update = 180 # Use PUB/SUB pattern for fanout methods. PUB/SUB always uses # proxy. (boolean value) #use_pub_sub = true -# Use ROUTER remote proxy for direct methods. (boolean value) +# Use ROUTER remote proxy. (boolean value) #use_router_proxy = true # Minimal port number for random ports range. (port value) @@ -299,12 +298,14 @@ #rpc_response_timeout = 60 # A URL representing the messaging driver to use and its full -# configuration. If not set, we fall back to the rpc_backend -# option and driver specific configuration. (string value) +# configuration. (string value) #transport_url = -# The messaging driver to use, defaults to rabbit. Other -# drivers include amqp and zmq. (string value) +# DEPRECATED: The messaging driver to use, defaults to rabbit. +# Other drivers include amqp and zmq. (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rpc_backend = rabbit # The default exchange under which topics are scoped. May be @@ -1319,7 +1320,7 @@ # Optionally specify a list of memcached server(s) to use for # caching. If left undefined, tokens will instead be cached # in-process. (list value) -# Deprecated group/name - [DEFAULT]/memcache_servers +# Deprecated group/name - [keystone_authtoken]/memcache_servers #memcached_servers = # In order to prevent excessive effort spent validating @@ -1410,7 +1411,7 @@ #hash_algorithms = md5 # Authentication type to load (unknown value) -# Deprecated group/name - [DEFAULT]/auth_plugin +# Deprecated group/name - [keystone_authtoken]/auth_plugin #auth_type = # Config Section from which to load plugin specific options @@ -1424,19 +1425,34 @@ # From oslo.messaging # -# Host to locate redis. (string value) +# DEPRECATED: Host to locate redis. (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #host = 127.0.0.1 -# Use this port to connect to redis host. (port value) +# DEPRECATED: Use this port to connect to redis host. (port +# value) # Minimum value: 0 # Maximum value: 65535 +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #port = 6379 -# Password for Redis server (optional). (string value) +# DEPRECATED: Password for Redis server (optional). (string +# value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #password = -# List of Redis Sentinel hosts (fault tolerance mode) e.g. -# [host:port, host1:port ... ] (list value) +# DEPRECATED: List of Redis Sentinel hosts (fault tolerance +# mode) e.g. [host:port, host1:port ... ] (list +# value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #sentinel_hosts = # Redis replica set name. (string value) @@ -1476,10 +1492,10 @@ # value) #retries = 3 -# Default authentication strategy to use when connecting to -# neutron. Running neutron in noauth mode (related to but not -# affected by this setting) is insecure and should only be -# used for testing. (string value) +# Authentication strategy to use when connecting to neutron. +# Running neutron in noauth mode (related to but not affected +# by this setting) is insecure and should only be used for +# testing. (string value) # Allowed values: keystone, noauth #auth_strategy = keystone @@ -1687,7 +1703,7 @@ # How long to wait a missing client beforce abandoning to send # it its replies. This value should not be longer than # rpc_response_timeout. (integer value) -# Deprecated group/name - [DEFAULT]/kombu_reconnect_timeout +# Deprecated group/name - [oslo_messaging_rabbit]/kombu_reconnect_timeout #kombu_missing_consumer_retry_timeout = 60 # Determines how the next RabbitMQ node is chosen in case the @@ -1697,40 +1713,59 @@ # Allowed values: round-robin, shuffle #kombu_failover_strategy = round-robin -# The RabbitMQ broker address where a single node is used. -# (string value) +# DEPRECATED: The RabbitMQ broker address where a single node +# is used. (string value) # Deprecated group/name - [DEFAULT]/rabbit_host +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_host = localhost -# The RabbitMQ broker port where a single node is used. (port -# value) +# DEPRECATED: The RabbitMQ broker port where a single node is +# used. (port value) # Minimum value: 0 # Maximum value: 65535 # Deprecated group/name - [DEFAULT]/rabbit_port +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_port = 5672 -# RabbitMQ HA cluster host:port pairs. (list value) +# DEPRECATED: RabbitMQ HA cluster host:port pairs. (list +# value) # Deprecated group/name - [DEFAULT]/rabbit_hosts +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_hosts = $rabbit_host:$rabbit_port # Connect over SSL for RabbitMQ. (boolean value) # Deprecated group/name - [DEFAULT]/rabbit_use_ssl #rabbit_use_ssl = false -# The RabbitMQ userid. (string value) +# DEPRECATED: The RabbitMQ userid. (string value) # Deprecated group/name - [DEFAULT]/rabbit_userid +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_userid = guest -# The RabbitMQ password. (string value) +# DEPRECATED: The RabbitMQ password. (string value) # Deprecated group/name - [DEFAULT]/rabbit_password +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_password = guest # The RabbitMQ login method. (string value) # Deprecated group/name - [DEFAULT]/rabbit_login_method #rabbit_login_method = AMQPLAIN -# The RabbitMQ virtual host. (string value) +# DEPRECATED: The RabbitMQ virtual host. (string value) # Deprecated group/name - [DEFAULT]/rabbit_virtual_host +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Replaced by [DEFAULT]/transport_url #rabbit_virtual_host = / # How frequently to retry connecting with RabbitMQ. (integer @@ -1815,6 +1850,10 @@ # error (floating point value) #host_connection_reconnect_delay = 0.25 +# Connection factory implementation (string value) +# Allowed values: new, single, read_write +#connection_factory = single + # Maximum number of connections to keep queued. (integer # value) #pool_max_size = 30 @@ -1840,7 +1879,7 @@ # Persist notification messages. (boolean value) #notification_persistence = false -# Exchange name for for sending notifications (string value) +# Exchange name for sending notifications (string value) #default_notification_exchange = ${control_exchange}_notification # Max number of not acknowledged message which RabbitMQ can diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py new file mode 100644 index 0000000000..dcd75e6e09 --- /dev/null +++ b/ironic/common/neutron.py @@ -0,0 +1,79 @@ +# 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 neutronclient.v2_0 import client as clientv20 +from oslo_config import cfg +from oslo_log import log + +from ironic.common.i18n import _ +from ironic.common import keystone + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF +CONF.import_opt('my_ip', 'ironic.netconf') + +neutron_opts = [ + cfg.StrOpt('url', + default='http://$my_ip:9696', + help=_('URL for connecting to neutron.')), + cfg.IntOpt('url_timeout', + default=30, + help=_('Timeout value for connecting to neutron in seconds.')), + cfg.IntOpt('port_setup_delay', + default=0, + min=0, + help=_('Delay value to wait for Neutron agents to setup ' + 'sufficient DHCP configuration for port.')), + cfg.IntOpt('retries', + default=3, + help=_('Client retries in the case of a failed request.')), + cfg.StrOpt('auth_strategy', + default='keystone', + choices=['keystone', 'noauth'], + help=_('Authentication strategy to use when connecting to ' + 'neutron. Running neutron in noauth mode (related to ' + 'but not affected by this setting) is insecure and ' + 'should only be used for testing.')), + cfg.StrOpt('cleaning_network_uuid', + help=_('UUID of the network to create Neutron ports on, when ' + 'booting to a ramdisk for cleaning using Neutron DHCP.')) +] + +CONF.register_opts(neutron_opts, group='neutron') + + +def get_client(token=None): + params = { + 'timeout': CONF.neutron.url_timeout, + 'retries': CONF.neutron.retries, + 'insecure': CONF.keystone_authtoken.insecure, + 'ca_cert': CONF.keystone_authtoken.certfile, + } + + if CONF.neutron.auth_strategy == 'noauth': + params['endpoint_url'] = CONF.neutron.url + params['auth_strategy'] = 'noauth' + else: + params['endpoint_url'] = ( + CONF.neutron.url or + keystone.get_service_url(service_type='network')) + params['username'] = CONF.keystone_authtoken.admin_user + params['tenant_name'] = CONF.keystone_authtoken.admin_tenant_name + params['password'] = CONF.keystone_authtoken.admin_password + params['auth_url'] = (CONF.keystone_authtoken.auth_uri or '') + if CONF.keystone.region_name: + params['region_name'] = CONF.keystone.region_name + params['token'] = token + + return clientv20.Client(**params) diff --git a/ironic/conf/opts.py b/ironic/conf/opts.py index 9914d1f28f..8541d95c76 100644 --- a/ironic/conf/opts.py +++ b/ironic/conf/opts.py @@ -21,11 +21,11 @@ import ironic.common.hash_ring import ironic.common.image_service import ironic.common.images import ironic.common.keystone +import ironic.common.neutron import ironic.common.paths import ironic.common.service import ironic.common.swift import ironic.common.utils -import ironic.dhcp.neutron import ironic.drivers.modules.agent import ironic.drivers.modules.agent_base_vendor import ironic.drivers.modules.agent_client @@ -97,7 +97,7 @@ _opts = [ ironic.drivers.modules.irmc.common.opts)), ('iscsi', ironic.drivers.modules.iscsi_deploy.iscsi_opts), ('keystone', ironic.common.keystone.keystone_opts), - ('neutron', ironic.dhcp.neutron.neutron_opts), + ('neutron', ironic.common.neutron.neutron_opts), ('oneview', ironic.drivers.modules.oneview.common.opts), ('pxe', itertools.chain( ironic.drivers.modules.iscsi_deploy.pxe_opts, diff --git a/ironic/dhcp/neutron.py b/ironic/dhcp/neutron.py index 7d55cefd9c..25f833ff02 100644 --- a/ironic/dhcp/neutron.py +++ b/ironic/dhcp/neutron.py @@ -17,7 +17,6 @@ import time from neutronclient.common import exceptions as neutron_client_exc -from neutronclient.v2_0 import client as clientv20 from oslo_config import cfg from oslo_log import log as logging from oslo_utils import netutils @@ -26,74 +25,16 @@ from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common.i18n import _LW -from ironic.common import keystone from ironic.common import network +from ironic.common import neutron from ironic.dhcp import base from ironic.drivers.modules import ssh -from ironic.objects.port import Port - - -neutron_opts = [ - cfg.StrOpt('url', - default='http://$my_ip:9696', - help=_('URL for connecting to neutron.')), - cfg.IntOpt('url_timeout', - default=30, - help=_('Timeout value for connecting to neutron in seconds.')), - cfg.IntOpt('port_setup_delay', - default=0, - min=0, - help=_('Delay value to wait for Neutron agents to setup ' - 'sufficient DHCP configuration for port.')), - cfg.IntOpt('retries', - default=3, - help=_('Client retries in the case of a failed request.')), - cfg.StrOpt('auth_strategy', - default='keystone', - choices=['keystone', 'noauth'], - help=_('Default authentication strategy to use when connecting ' - 'to neutron. ' - 'Running neutron in noauth mode (related to but not ' - 'affected by this setting) is insecure and should only ' - 'be used for testing.')), - cfg.StrOpt('cleaning_network_uuid', - help=_('UUID of the network to create Neutron ports on, when ' - 'booting to a ramdisk for cleaning using Neutron DHCP.')) -] +from ironic import objects CONF = cfg.CONF -CONF.import_opt('my_ip', 'ironic.netconf') -CONF.register_opts(neutron_opts, group='neutron') LOG = logging.getLogger(__name__) -def _build_client(token=None): - """Utility function to create Neutron client.""" - params = { - 'timeout': CONF.neutron.url_timeout, - 'retries': CONF.neutron.retries, - 'insecure': CONF.keystone_authtoken.insecure, - 'ca_cert': CONF.keystone_authtoken.certfile, - } - - if CONF.neutron.auth_strategy == 'noauth': - params['endpoint_url'] = CONF.neutron.url - params['auth_strategy'] = 'noauth' - else: - params['endpoint_url'] = ( - CONF.neutron.url or - keystone.get_service_url(service_type='network')) - params['username'] = CONF.keystone_authtoken.admin_user - params['tenant_name'] = CONF.keystone_authtoken.admin_tenant_name - params['password'] = CONF.keystone_authtoken.admin_password - params['auth_url'] = (CONF.keystone_authtoken.auth_uri or '') - if CONF.keystone.region_name: - params['region_name'] = CONF.keystone.region_name - params['token'] = token - - return clientv20.Client(**params) - - class NeutronDHCPApi(base.BaseDHCP): """API for communicating to neutron 2.x API.""" @@ -122,7 +63,7 @@ class NeutronDHCPApi(base.BaseDHCP): """ port_req_body = {'port': {'extra_dhcp_opts': dhcp_options}} try: - _build_client(token).update_port(port_id, port_req_body) + neutron.get_client(token).update_port(port_id, port_req_body) except neutron_client_exc.NeutronClientException: LOG.exception(_LE("Failed to update Neutron port %s."), port_id) raise exception.FailedToUpdateDHCPOptOnPort(port_id=port_id) @@ -137,7 +78,7 @@ class NeutronDHCPApi(base.BaseDHCP): """ port_req_body = {'port': {'mac_address': address}} try: - _build_client(token).update_port(port_id, port_req_body) + neutron.get_client(token).update_port(port_id, port_req_body) except neutron_client_exc.NeutronClientException: LOG.exception(_LE("Failed to update MAC address on Neutron " "port %s."), port_id) @@ -267,7 +208,7 @@ class NeutronDHCPApi(base.BaseDHCP): vif = p_obj.extra.get('vif_port_id') if not vif: obj_name = 'portgroup' - if isinstance(p_obj, Port): + if isinstance(p_obj, objects.Port): obj_name = 'port' LOG.warning(_LW("No VIFs found for node %(node)s when attempting " "to get IP address for %(obj_name)s: %(obj_id)."), @@ -300,7 +241,7 @@ class NeutronDHCPApi(base.BaseDHCP): if failures: obj_name = 'portgroups' - if isinstance(pobj_list[0], Port): + if isinstance(pobj_list[0], objects.Port): obj_name = 'ports' LOG.warning(_LW( @@ -319,7 +260,7 @@ class NeutronDHCPApi(base.BaseDHCP): :returns: List of IP addresses associated with task's ports/portgroups. """ - client = _build_client(task.context.auth_token) + client = neutron.get_client(task.context.auth_token) port_ip_addresses = self._get_ip_addresses(task, task.ports, client) portgroup_ip_addresses = self._get_ip_addresses( @@ -337,7 +278,7 @@ class NeutronDHCPApi(base.BaseDHCP): if not CONF.neutron.cleaning_network_uuid: raise exception.InvalidParameterValue(_('Valid cleaning network ' 'UUID not provided')) - neutron_client = _build_client(task.context.auth_token) + neutron_client = neutron.get_client(task.context.auth_token) body = { 'port': { 'network_id': CONF.neutron.cleaning_network_uuid, @@ -373,7 +314,7 @@ class NeutronDHCPApi(base.BaseDHCP): :param task: a TaskManager instance. """ - neutron_client = _build_client(task.context.auth_token) + neutron_client = neutron.get_client(task.context.auth_token) macs = [p.address for p in task.ports] params = { 'network_id': CONF.neutron.cleaning_network_uuid diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py new file mode 100644 index 0000000000..c55c115a3a --- /dev/null +++ b/ironic/tests/unit/common/test_neutron.py @@ -0,0 +1,109 @@ +# 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 mock +from neutronclient.v2_0 import client +from oslo_config import cfg + +from ironic.common import neutron +from ironic.tests import base + + +class TestNeutronClient(base.TestCase): + + def setUp(self): + super(TestNeutronClient, self).setUp() + self.config(url='test-url', + url_timeout=30, + retries=2, + group='neutron') + self.config(insecure=False, + certfile='test-file', + admin_user='test-admin-user', + admin_tenant_name='test-admin-tenant', + admin_password='test-admin-password', + auth_uri='test-auth-uri', + group='keystone_authtoken') + + @mock.patch.object(client.Client, "__init__") + def test_get_neutron_client_with_token(self, mock_client_init): + token = 'test-token-123' + expected = {'timeout': 30, + 'retries': 2, + 'insecure': False, + 'ca_cert': 'test-file', + 'token': token, + 'endpoint_url': 'test-url', + 'username': 'test-admin-user', + 'tenant_name': 'test-admin-tenant', + 'password': 'test-admin-password', + 'auth_url': 'test-auth-uri'} + + mock_client_init.return_value = None + neutron.get_client(token=token) + mock_client_init.assert_called_once_with(**expected) + + @mock.patch.object(client.Client, "__init__") + def test_get_neutron_client_without_token(self, mock_client_init): + expected = {'timeout': 30, + 'retries': 2, + 'insecure': False, + 'ca_cert': 'test-file', + 'token': None, + 'endpoint_url': 'test-url', + 'username': 'test-admin-user', + 'tenant_name': 'test-admin-tenant', + 'password': 'test-admin-password', + 'auth_url': 'test-auth-uri'} + + mock_client_init.return_value = None + neutron.get_client(token=None) + mock_client_init.assert_called_once_with(**expected) + + @mock.patch.object(client.Client, "__init__") + def test_get_neutron_client_with_region(self, mock_client_init): + expected = {'timeout': 30, + 'retries': 2, + 'insecure': False, + 'ca_cert': 'test-file', + 'token': None, + 'endpoint_url': 'test-url', + 'username': 'test-admin-user', + 'tenant_name': 'test-admin-tenant', + 'password': 'test-admin-password', + 'auth_url': 'test-auth-uri', + 'region_name': 'test-region'} + + self.config(region_name='test-region', + group='keystone') + mock_client_init.return_value = None + neutron.get_client(token=None) + mock_client_init.assert_called_once_with(**expected) + + @mock.patch.object(client.Client, "__init__") + def test_get_neutron_client_noauth(self, mock_client_init): + self.config(auth_strategy='noauth', group='neutron') + expected = {'ca_cert': 'test-file', + 'insecure': False, + 'endpoint_url': 'test-url', + 'timeout': 30, + 'retries': 2, + 'auth_strategy': 'noauth'} + + mock_client_init.return_value = None + neutron.get_client(token=None) + mock_client_init.assert_called_once_with(**expected) + + def test_out_range_auth_strategy(self): + self.assertRaises(ValueError, cfg.CONF.set_override, + 'auth_strategy', 'fake', 'neutron', + enforce_type=True) diff --git a/ironic/tests/unit/dhcp/test_neutron.py b/ironic/tests/unit/dhcp/test_neutron.py index 2647639e71..658d334976 100644 --- a/ironic/tests/unit/dhcp/test_neutron.py +++ b/ironic/tests/unit/dhcp/test_neutron.py @@ -66,75 +66,6 @@ class TestNeutron(db_base.DbTestCase): dhcp_factory.DHCPFactory._dhcp_provider = None - @mock.patch.object(client.Client, "__init__") - def test__build_client_with_token(self, mock_client_init): - token = 'test-token-123' - expected = {'timeout': 30, - 'retries': 2, - 'insecure': False, - 'ca_cert': 'test-file', - 'token': token, - 'endpoint_url': 'test-url', - 'username': 'test-admin-user', - 'tenant_name': 'test-admin-tenant', - 'password': 'test-admin-password', - 'auth_url': 'test-auth-uri'} - - mock_client_init.return_value = None - neutron._build_client(token=token) - mock_client_init.assert_called_once_with(**expected) - - @mock.patch.object(client.Client, "__init__") - def test__build_client_without_token(self, mock_client_init): - expected = {'timeout': 30, - 'retries': 2, - 'insecure': False, - 'ca_cert': 'test-file', - 'token': None, - 'endpoint_url': 'test-url', - 'username': 'test-admin-user', - 'tenant_name': 'test-admin-tenant', - 'password': 'test-admin-password', - 'auth_url': 'test-auth-uri'} - - mock_client_init.return_value = None - neutron._build_client(token=None) - mock_client_init.assert_called_once_with(**expected) - - @mock.patch.object(client.Client, "__init__") - def test__build_client_with_region(self, mock_client_init): - expected = {'timeout': 30, - 'retries': 2, - 'insecure': False, - 'ca_cert': 'test-file', - 'token': None, - 'endpoint_url': 'test-url', - 'username': 'test-admin-user', - 'tenant_name': 'test-admin-tenant', - 'password': 'test-admin-password', - 'auth_url': 'test-auth-uri', - 'region_name': 'test-region'} - - self.config(region_name='test-region', - group='keystone') - mock_client_init.return_value = None - neutron._build_client(token=None) - mock_client_init.assert_called_once_with(**expected) - - @mock.patch.object(client.Client, "__init__") - def test__build_client_noauth(self, mock_client_init): - self.config(auth_strategy='noauth', group='neutron') - expected = {'ca_cert': 'test-file', - 'insecure': False, - 'endpoint_url': 'test-url', - 'timeout': 30, - 'retries': 2, - 'auth_strategy': 'noauth'} - - mock_client_init.return_value = None - neutron._build_client(token=None) - mock_client_init.assert_called_once_with(**expected) - @mock.patch.object(client.Client, 'update_port') @mock.patch.object(client.Client, "__init__") def test_update_port_dhcp_opts(self, mock_client_init, mock_update_port):