add --fixed-ip argument to create port
add --fixed-ip argument to create port and add list and dict type for unknow option now we can use known option feature: quantumv2 create_port --fixed-ip subnet_id=<id>,ip_address=<ip> --fixed-ip subnet_id=<id>, ip_address=<ip2> network_id or unknown option feature: one ip: quantumv2 create_port network_id --fixed_ips type=dict list=true subnet_id=<id>,ip_address=<ip> two ips: quantumv2 create_port network_id --fixed_ips type=dict subnet_id=<id>,ip_address=<ip> subnet_id=<id>,ip_address=<ip2> to create port Please download: https://review.openstack.org/#/c/8794/4 and set core_plugin = quantum.db.db_base_plugin_v2.QuantumDbPluginV2 on quantum server side Patch 2: support cliff 1.0 Patch 3: support specify auth strategy, for now, any other auth strategy than keystone will disable auth, format port output Patch 4: format None as '' when outputing, deal with list of dict, add QUANTUMCLIENT_DEBUG env to enable http req/resp print, which is helpful for testing nova integration Patch 5: fix interactive mode, and initialize_app problem Change-Id: I693848c75055d1947862d55f4b538c1dfb1e86db
This commit is contained in:
parent
dd803f8e26
commit
50c46f61d1
@ -20,6 +20,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import urlparse
|
import urlparse
|
||||||
# Python 2.5 compat fix
|
# Python 2.5 compat fix
|
||||||
if not hasattr(urlparse, 'parse_qsl'):
|
if not hasattr(urlparse, 'parse_qsl'):
|
||||||
@ -33,6 +34,11 @@ from quantumclient.common import utils
|
|||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if 'QUANTUMCLIENT_DEBUG' in os.environ and os.environ['QUANTUMCLIENT_DEBUG']:
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
_logger.addHandler(ch)
|
||||||
|
|
||||||
|
|
||||||
class ServiceCatalog(object):
|
class ServiceCatalog(object):
|
||||||
"""Helper methods for dealing with a Keystone Service Catalog."""
|
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||||
@ -86,7 +92,8 @@ class HTTPClient(httplib2.Http):
|
|||||||
def __init__(self, username=None, tenant_name=None,
|
def __init__(self, username=None, tenant_name=None,
|
||||||
password=None, auth_url=None,
|
password=None, auth_url=None,
|
||||||
token=None, region_name=None, timeout=None,
|
token=None, region_name=None, timeout=None,
|
||||||
endpoint_url=None, insecure=False, **kwargs):
|
endpoint_url=None, insecure=False,
|
||||||
|
auth_strategy='keystone', **kwargs):
|
||||||
super(HTTPClient, self).__init__(timeout=timeout)
|
super(HTTPClient, self).__init__(timeout=timeout)
|
||||||
self.username = username
|
self.username = username
|
||||||
self.tenant_name = tenant_name
|
self.tenant_name = tenant_name
|
||||||
@ -96,6 +103,7 @@ class HTTPClient(httplib2.Http):
|
|||||||
self.auth_token = token
|
self.auth_token = token
|
||||||
self.content_type = 'application/json'
|
self.content_type = 'application/json'
|
||||||
self.endpoint_url = endpoint_url
|
self.endpoint_url = endpoint_url
|
||||||
|
self.auth_strategy = auth_strategy
|
||||||
# httplib2 overrides
|
# httplib2 overrides
|
||||||
self.force_exception_to_status_code = True
|
self.force_exception_to_status_code = True
|
||||||
self.disable_ssl_certificate_validation = insecure
|
self.disable_ssl_certificate_validation = insecure
|
||||||
@ -126,19 +134,21 @@ class HTTPClient(httplib2.Http):
|
|||||||
return resp, body
|
return resp, body
|
||||||
|
|
||||||
def do_request(self, url, method, **kwargs):
|
def do_request(self, url, method, **kwargs):
|
||||||
if not self.endpoint_url or not self.auth_token:
|
if not self.endpoint_url:
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
|
||||||
# Perform the request once. If we get a 401 back then it
|
# Perform the request once. If we get a 401 back then it
|
||||||
# might be because the auth token expired, so try to
|
# might be because the auth token expired, so try to
|
||||||
# re-authenticate and try again. If it still fails, bail.
|
# re-authenticate and try again. If it still fails, bail.
|
||||||
try:
|
try:
|
||||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
if self.auth_token:
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
||||||
resp, body = self._cs_request(self.endpoint_url + url, method,
|
resp, body = self._cs_request(self.endpoint_url + url, method,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
return resp, body
|
return resp, body
|
||||||
except exceptions.Unauthorized as ex:
|
except exceptions.Unauthorized as ex:
|
||||||
if not self.endpoint_url or not self.auth_token:
|
if not self.endpoint_url:
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
resp, body = self._cs_request(
|
resp, body = self._cs_request(
|
||||||
self.management_url + url, method, **kwargs)
|
self.management_url + url, method, **kwargs)
|
||||||
@ -161,6 +171,8 @@ class HTTPClient(httplib2.Http):
|
|||||||
endpoint_type='adminURL')
|
endpoint_type='adminURL')
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
|
if self.auth_strategy != 'keystone':
|
||||||
|
raise exceptions.Unauthorized(message='unknown auth strategy')
|
||||||
body = {'auth': {'passwordCredentials':
|
body = {'auth': {'passwordCredentials':
|
||||||
{'username': self.username,
|
{'username': self.username,
|
||||||
'password': self.password, },
|
'password': self.password, },
|
||||||
|
@ -53,6 +53,7 @@ class ClientManager(object):
|
|||||||
username=None, password=None,
|
username=None, password=None,
|
||||||
region_name=None,
|
region_name=None,
|
||||||
api_version=None,
|
api_version=None,
|
||||||
|
auth_strategy=None
|
||||||
):
|
):
|
||||||
self._token = token
|
self._token = token
|
||||||
self._url = url
|
self._url = url
|
||||||
@ -64,6 +65,7 @@ class ClientManager(object):
|
|||||||
self._region_name = region_name
|
self._region_name = region_name
|
||||||
self._api_version = api_version
|
self._api_version = api_version
|
||||||
self._service_catalog = None
|
self._service_catalog = None
|
||||||
|
self._auth_strategy = auth_strategy
|
||||||
return
|
return
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
|
@ -33,3 +33,9 @@ class OpenStackCommand(Command):
|
|||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return super(OpenStackCommand, self).run(parsed_args)
|
return super(OpenStackCommand, self).run(parsed_args)
|
||||||
|
|
||||||
|
def get_data(self, parsed_args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def take_action(self, parsed_args):
|
||||||
|
return self.get_data(parsed_args)
|
||||||
|
@ -62,9 +62,9 @@ def to_primitive(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def dumps(value):
|
def dumps(value, indent=None):
|
||||||
try:
|
try:
|
||||||
return json.dumps(value)
|
return json.dumps(value, indent=indent)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
return json.dumps(to_primitive(value))
|
return json.dumps(to_primitive(value))
|
||||||
@ -126,17 +126,28 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
|||||||
data = item[field_name]
|
data = item[field_name]
|
||||||
else:
|
else:
|
||||||
data = getattr(item, field_name, '')
|
data = getattr(item, field_name, '')
|
||||||
|
if data is None:
|
||||||
|
data = ''
|
||||||
row.append(data)
|
row.append(data)
|
||||||
return tuple(row)
|
return tuple(row)
|
||||||
|
|
||||||
|
|
||||||
def __str2bool(strbool):
|
def str2bool(strbool):
|
||||||
if strbool is None:
|
if strbool is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return strbool.lower() == 'true'
|
return strbool.lower() == 'true'
|
||||||
|
|
||||||
|
|
||||||
|
def str2dict(strdict):
|
||||||
|
'''@param strdict: key1=value1,key2=value2'''
|
||||||
|
_info = {}
|
||||||
|
for kv_str in strdict.split(","):
|
||||||
|
k, v = kv_str.split("=", 1)
|
||||||
|
_info.update({k: v})
|
||||||
|
return _info
|
||||||
|
|
||||||
|
|
||||||
def http_log(_logger, args, kwargs, resp, body):
|
def http_log(_logger, args, kwargs, resp, body):
|
||||||
if not _logger.isEnabledFor(logging.DEBUG):
|
if not _logger.isEnabledFor(logging.DEBUG):
|
||||||
return
|
return
|
||||||
|
@ -64,5 +64,6 @@ def make_client(instance):
|
|||||||
region_name=instance._region_name,
|
region_name=instance._region_name,
|
||||||
auth_url=instance._auth_url,
|
auth_url=instance._auth_url,
|
||||||
endpoint_url=url,
|
endpoint_url=url,
|
||||||
token=instance._token)
|
token=instance._token,
|
||||||
|
auth_strategy=instance._auth_strategy)
|
||||||
return client
|
return client
|
||||||
|
@ -70,11 +70,12 @@ def parse_args_to_dict(values_specs):
|
|||||||
current_arg = None
|
current_arg = None
|
||||||
_values_specs = []
|
_values_specs = []
|
||||||
_value_number = 0
|
_value_number = 0
|
||||||
|
_list_flag = False
|
||||||
current_item = None
|
current_item = None
|
||||||
for _item in values_specs:
|
for _item in values_specs:
|
||||||
if _item.startswith('--'):
|
if _item.startswith('--'):
|
||||||
if current_arg is not None:
|
if current_arg is not None:
|
||||||
if _value_number > 1:
|
if _value_number > 1 or _list_flag:
|
||||||
current_arg.update({'nargs': '+'})
|
current_arg.update({'nargs': '+'})
|
||||||
elif _value_number == 0:
|
elif _value_number == 0:
|
||||||
current_arg.update({'action': 'store_true'})
|
current_arg.update({'action': 'store_true'})
|
||||||
@ -93,12 +94,16 @@ def parse_args_to_dict(values_specs):
|
|||||||
_type_str = _item.split('=', 2)[1]
|
_type_str = _item.split('=', 2)[1]
|
||||||
current_arg.update({'type': eval(_type_str)})
|
current_arg.update({'type': eval(_type_str)})
|
||||||
if _type_str == 'bool':
|
if _type_str == 'bool':
|
||||||
current_arg.update({'type': utils.__str2bool})
|
current_arg.update({'type': utils.str2bool})
|
||||||
|
elif _type_str == 'dict':
|
||||||
|
current_arg.update({'type': utils.str2dict})
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
"invalid values_specs %s" % ' '.join(values_specs))
|
"invalid values_specs %s" % ' '.join(values_specs))
|
||||||
|
elif _item == 'list=true':
|
||||||
|
_list_flag = True
|
||||||
|
continue
|
||||||
if not _item.startswith('--'):
|
if not _item.startswith('--'):
|
||||||
if not current_item or '=' in current_item:
|
if not current_item or '=' in current_item:
|
||||||
raise exceptions.CommandError(
|
raise exceptions.CommandError(
|
||||||
@ -110,9 +115,10 @@ def parse_args_to_dict(values_specs):
|
|||||||
_value_number = 1
|
_value_number = 1
|
||||||
else:
|
else:
|
||||||
_value_number = 0
|
_value_number = 0
|
||||||
|
_list_flag = False
|
||||||
_values_specs.append(_item)
|
_values_specs.append(_item)
|
||||||
if current_arg is not None:
|
if current_arg is not None:
|
||||||
if _value_number > 1:
|
if _value_number > 1 or _list_flag:
|
||||||
current_arg.update({'nargs': '+'})
|
current_arg.update({'nargs': '+'})
|
||||||
elif _value_number == 0:
|
elif _value_number == 0:
|
||||||
current_arg.update({'action': 'store_true'})
|
current_arg.update({'action': 'store_true'})
|
||||||
@ -188,6 +194,19 @@ class CreateCommand(QuantumCommand, show.ShowOne):
|
|||||||
print >>self.app.stdout, _('Created a new %s:' % self.resource)
|
print >>self.app.stdout, _('Created a new %s:' % self.resource)
|
||||||
else:
|
else:
|
||||||
info = {'': ''}
|
info = {'': ''}
|
||||||
|
for k, v in info.iteritems():
|
||||||
|
if isinstance(v, list):
|
||||||
|
value = ""
|
||||||
|
for _item in v:
|
||||||
|
if value:
|
||||||
|
value += "\n"
|
||||||
|
if isinstance(_item, dict):
|
||||||
|
value += utils.dumps(_item)
|
||||||
|
else:
|
||||||
|
value += str(_item)
|
||||||
|
info[k] = value
|
||||||
|
elif v is None:
|
||||||
|
info[k] = ''
|
||||||
return zip(*sorted(info.iteritems()))
|
return zip(*sorted(info.iteritems()))
|
||||||
|
|
||||||
|
|
||||||
@ -334,6 +353,19 @@ class ShowCommand(QuantumCommand, show.ShowOne):
|
|||||||
"show_%s" % self.resource)
|
"show_%s" % self.resource)
|
||||||
data = obj_showor(parsed_args.id, **params)
|
data = obj_showor(parsed_args.id, **params)
|
||||||
if self.resource in data:
|
if self.resource in data:
|
||||||
|
for k, v in data[self.resource].iteritems():
|
||||||
|
if isinstance(v, list):
|
||||||
|
value = ""
|
||||||
|
for _item in v:
|
||||||
|
if value:
|
||||||
|
value += "\n"
|
||||||
|
if isinstance(_item, dict):
|
||||||
|
value += utils.dumps(_item)
|
||||||
|
else:
|
||||||
|
value += str(_item)
|
||||||
|
data[self.resource][k] = value
|
||||||
|
elif v is None:
|
||||||
|
data[self.resource][k] = ''
|
||||||
return zip(*sorted(data[self.resource].iteritems()))
|
return zip(*sorted(data[self.resource].iteritems()))
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from quantumclient import utils
|
||||||
from quantumclient.quantum.v2_0 import CreateCommand
|
from quantumclient.quantum.v2_0 import CreateCommand
|
||||||
from quantumclient.quantum.v2_0 import DeleteCommand
|
from quantumclient.quantum.v2_0 import DeleteCommand
|
||||||
from quantumclient.quantum.v2_0 import ListCommand
|
from quantumclient.quantum.v2_0 import ListCommand
|
||||||
@ -26,7 +27,7 @@ from quantumclient.quantum.v2_0 import UpdateCommand
|
|||||||
|
|
||||||
def _format_fixed_ips(port):
|
def _format_fixed_ips(port):
|
||||||
try:
|
try:
|
||||||
return '\n'.join(port['fixed_ips'])
|
return '\n'.join([utils.dumps(ip) for ip in port['fixed_ips']])
|
||||||
except Exception:
|
except Exception:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -74,6 +75,12 @@ class CreatePort(CreateCommand):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--device-id',
|
'--device-id',
|
||||||
help='device id of this port')
|
help='device id of this port')
|
||||||
|
parser.add_argument(
|
||||||
|
'--fixed-ip',
|
||||||
|
action='append',
|
||||||
|
help='desired Ip for this port: '
|
||||||
|
'subnet_id=<id>,ip_address=<ip>, '
|
||||||
|
'can be repeated')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'network_id',
|
'network_id',
|
||||||
help='Network id of this port belongs to')
|
help='Network id of this port belongs to')
|
||||||
@ -87,6 +94,12 @@ class CreatePort(CreateCommand):
|
|||||||
body['port'].update({'device_id': parsed_args.device_id})
|
body['port'].update({'device_id': parsed_args.device_id})
|
||||||
if parsed_args.tenant_id:
|
if parsed_args.tenant_id:
|
||||||
body['port'].update({'tenant_id': parsed_args.tenant_id})
|
body['port'].update({'tenant_id': parsed_args.tenant_id})
|
||||||
|
ips = []
|
||||||
|
if parsed_args.fixed_ip:
|
||||||
|
for ip_spec in parsed_args.fixed_ip:
|
||||||
|
ips.append(utils.str2dict(ip_spec))
|
||||||
|
if ips:
|
||||||
|
body['port'].update({'fixed_ips': ips})
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
@ -197,6 +197,13 @@ class QuantumShell(App):
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='show tracebacks on errors', )
|
help='show tracebacks on errors', )
|
||||||
# Global arguments
|
# Global arguments
|
||||||
|
parser.add_argument(
|
||||||
|
'--os-auth-strategy', metavar='<auth-strategy>',
|
||||||
|
default=env('OS_AUTH_STRATEGY', default='keystone'),
|
||||||
|
help='Authentication strategy (Env: OS_AUTH_STRATEGY'
|
||||||
|
', default keystone). For now, any other value will'
|
||||||
|
' disable the authentication')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-auth-url', metavar='<auth-url>',
|
'--os-auth-url', metavar='<auth-url>',
|
||||||
default=env('OS_AUTH_URL'),
|
default=env('OS_AUTH_URL'),
|
||||||
@ -272,41 +279,46 @@ class QuantumShell(App):
|
|||||||
"""Make sure the user has provided all of the authentication
|
"""Make sure the user has provided all of the authentication
|
||||||
info we need.
|
info we need.
|
||||||
"""
|
"""
|
||||||
|
if self.options.os_auth_strategy == 'keystone':
|
||||||
|
if self.options.os_token or self.options.os_url:
|
||||||
|
# Token flow auth takes priority
|
||||||
|
if not self.options.os_token:
|
||||||
|
raise exc.CommandError(
|
||||||
|
"You must provide a token via"
|
||||||
|
" either --os-token or env[OS_TOKEN]")
|
||||||
|
|
||||||
if self.options.os_token or self.options.os_url:
|
if not self.options.os_url:
|
||||||
# Token flow auth takes priority
|
raise exc.CommandError(
|
||||||
if not self.options.os_token:
|
"You must provide a service URL via"
|
||||||
raise exc.CommandError(
|
" either --os-url or env[OS_URL]")
|
||||||
"You must provide a token via"
|
|
||||||
" either --os-token or env[OS_TOKEN]")
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Validate password flow auth
|
||||||
|
if not self.options.os_username:
|
||||||
|
raise exc.CommandError(
|
||||||
|
"You must provide a username via"
|
||||||
|
" either --os-username or env[OS_USERNAME]")
|
||||||
|
|
||||||
|
if not self.options.os_password:
|
||||||
|
raise exc.CommandError(
|
||||||
|
"You must provide a password via"
|
||||||
|
" either --os-password or env[OS_PASSWORD]")
|
||||||
|
|
||||||
|
if not (self.options.os_tenant_name):
|
||||||
|
raise exc.CommandError(
|
||||||
|
"You must provide a tenant_name via"
|
||||||
|
" either --os-tenant-name or via env[OS_TENANT_NAME]")
|
||||||
|
|
||||||
|
if not self.options.os_auth_url:
|
||||||
|
raise exc.CommandError(
|
||||||
|
"You must provide an auth url via"
|
||||||
|
" either --os-auth-url or via env[OS_AUTH_URL]")
|
||||||
|
else: # not keystone
|
||||||
if not self.options.os_url:
|
if not self.options.os_url:
|
||||||
raise exc.CommandError(
|
raise exc.CommandError(
|
||||||
"You must provide a service URL via"
|
"You must provide a service URL via"
|
||||||
" either --os-url or env[OS_URL]")
|
" either --os-url or env[OS_URL]")
|
||||||
|
|
||||||
else:
|
|
||||||
# Validate password flow auth
|
|
||||||
if not self.options.os_username:
|
|
||||||
raise exc.CommandError(
|
|
||||||
"You must provide a username via"
|
|
||||||
" either --os-username or env[OS_USERNAME]")
|
|
||||||
|
|
||||||
if not self.options.os_password:
|
|
||||||
raise exc.CommandError(
|
|
||||||
"You must provide a password via"
|
|
||||||
" either --os-password or env[OS_PASSWORD]")
|
|
||||||
|
|
||||||
if not (self.options.os_tenant_name):
|
|
||||||
raise exc.CommandError(
|
|
||||||
"You must provide a tenant_name via"
|
|
||||||
" either --os-tenant-name or via env[OS_TENANT_NAME]")
|
|
||||||
|
|
||||||
if not self.options.os_auth_url:
|
|
||||||
raise exc.CommandError(
|
|
||||||
"You must provide an auth url via"
|
|
||||||
" either --os-auth-url or via env[OS_AUTH_URL]")
|
|
||||||
|
|
||||||
self.client_manager = clientmanager.ClientManager(
|
self.client_manager = clientmanager.ClientManager(
|
||||||
token=self.options.os_token,
|
token=self.options.os_token,
|
||||||
url=self.options.os_url,
|
url=self.options.os_url,
|
||||||
@ -315,7 +327,8 @@ class QuantumShell(App):
|
|||||||
username=self.options.os_username,
|
username=self.options.os_username,
|
||||||
password=self.options.os_password,
|
password=self.options.os_password,
|
||||||
region_name=self.options.os_region_name,
|
region_name=self.options.os_region_name,
|
||||||
api_version=self.api_version, )
|
api_version=self.api_version,
|
||||||
|
auth_strategy=self.options.os_auth_strategy, )
|
||||||
return
|
return
|
||||||
|
|
||||||
def initialize_app(self, argv):
|
def initialize_app(self, argv):
|
||||||
|
@ -57,3 +57,16 @@ class CLITestArgs(unittest.TestCase):
|
|||||||
_specs = ['--tag=t', '--arg1', 'value1']
|
_specs = ['--tag=t', '--arg1', 'value1']
|
||||||
self.assertEqual('value1',
|
self.assertEqual('value1',
|
||||||
quantumV20.parse_args_to_dict(_specs)['arg1'])
|
quantumV20.parse_args_to_dict(_specs)['arg1'])
|
||||||
|
|
||||||
|
def test_dict_arg(self):
|
||||||
|
_specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2']
|
||||||
|
arg1 = quantumV20.parse_args_to_dict(_specs)['arg1']
|
||||||
|
self.assertEqual('value1', arg1['key1'])
|
||||||
|
self.assertEqual('value2', arg1['key2'])
|
||||||
|
|
||||||
|
def test_list_of_dict_arg(self):
|
||||||
|
_specs = ['--tag=t', '--arg1', 'type=dict',
|
||||||
|
'list=true', 'key1=value1,key2=value2']
|
||||||
|
arg1 = quantumV20.parse_args_to_dict(_specs)['arg1']
|
||||||
|
self.assertEqual('value1', arg1[0]['key1'])
|
||||||
|
self.assertEqual('value2', arg1[0]['key2'])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user