ipalib.frontend: Do API version check before converting arguments

This results in the proper message being shown if the client sends
an option the server doesn't have yet.

It also adds the check to commands that override run() but not __call__,
such as `ipa ping`, and to commands run on the server. Adjust tests
for these changes.

https://fedorahosted.org/freeipa/ticket/3963

Reviewed-By: Martin Kosek <mkosek@redhat.com>
This commit is contained in:
Petr Viktorin
2014-06-06 14:21:16 +02:00
committed by Martin Kosek
parent 91d3d4d7b2
commit ba53299b98
3 changed files with 24 additions and 22 deletions

View File

@@ -140,7 +140,7 @@ implement a ``run()`` method, like this:
>>> api = create_api() >>> api = create_api()
>>> api.register(my_command) >>> api.register(my_command)
>>> api.finalize() >>> api.finalize()
>>> api.Command.my_command() # Call your command >>> api.Command.my_command(version=u'2.47') # Call your command
{'result': 'My run() method was called!'} {'result': 'My run() method was called!'}
When `frontend.Command.__call__()` is called, it first validates any arguments When `frontend.Command.__call__()` is called, it first validates any arguments
@@ -359,7 +359,7 @@ False
And yet we can call ``my_command()``: And yet we can call ``my_command()``:
>>> api.Command.my_command() >>> api.Command.my_command(version=u'2.47')
{'result': 'Just my_command.forward() getting called here.'} {'result': 'Just my_command.forward() getting called here.'}

View File

@@ -419,6 +419,11 @@ class Command(HasParam):
XML-RPC and the executed an the nearest IPA server. XML-RPC and the executed an the nearest IPA server.
""" """
self.ensure_finalized() self.ensure_finalized()
version_provided = 'version' in options
if version_provided:
self.verify_client_version(unicode(options['version']))
else:
options['version'] = API_VERSION
params = self.args_options_2_params(*args, **options) params = self.args_options_2_params(*args, **options)
self.debug( self.debug(
'raw: %s(%s)', self.name, ', '.join(self._repr_iter(**params)) 'raw: %s(%s)', self.name, ', '.join(self._repr_iter(**params))
@@ -429,11 +434,13 @@ class Command(HasParam):
self.debug( self.debug(
'%s(%s)', self.name, ', '.join(self._repr_iter(**params)) '%s(%s)', self.name, ', '.join(self._repr_iter(**params))
) )
if not self.api.env.in_server and 'version' not in params:
params['version'] = API_VERSION
self.validate(**params) self.validate(**params)
(args, options) = self.params_2_args_options(**params) (args, options) = self.params_2_args_options(**params)
ret = self.run(*args, **options) ret = self.run(*args, **options)
if not version_provided and isinstance(ret, dict):
messages.add_message(
API_VERSION, ret,
messages.VersionMissing(server_version=API_VERSION))
if ( if (
isinstance(ret, dict) isinstance(ret, dict)
and 'summary' in self.output and 'summary' in self.output
@@ -441,7 +448,7 @@ class Command(HasParam):
): ):
ret['summary'] = self.get_summary_default(ret) ret['summary'] = self.get_summary_default(ret)
if self.use_output_validation and (self.output or ret is not None): if self.use_output_validation and (self.output or ret is not None):
self.validate_output(ret, options.get('version', API_VERSION)) self.validate_output(ret, options['version'])
return ret return ret
def soft_validate(self, values): def soft_validate(self, values):
@@ -744,17 +751,7 @@ class Command(HasParam):
performs is executed remotely. performs is executed remotely.
""" """
if self.api.env.in_server: if self.api.env.in_server:
version_provided = 'version' in options return self.execute(*args, **options)
if version_provided:
self.verify_client_version(options['version'])
else:
options['version'] = API_VERSION
result = self.execute(*args, **options)
if not version_provided:
messages.add_message(
API_VERSION, result,
messages.VersionMissing(server_version=API_VERSION))
return result
return self.forward(*args, **options) return self.forward(*args, **options)
def execute(self, *args, **kw): def execute(self, *args, **kw):
@@ -1318,15 +1315,18 @@ class Method(Attribute, Command):
>>> list(api.Method) >>> list(api.Method)
['user_add'] ['user_add']
>>> api.Method.user_add() # Will call user_add.run() >>> api.Method.user_add(version=u'2.88') # Will call user_add.run()
{'result': 'Added the user!'} {'result': 'Added the user!'}
(The "version" argument is the API version to use.
The current API version can be found in ipalib.version.API_VERSION.)
Second, because `Method` is a subclass of `Command`, the ``user_add`` Second, because `Method` is a subclass of `Command`, the ``user_add``
plugin can also be accessed through the ``api.Command`` namespace: plugin can also be accessed through the ``api.Command`` namespace:
>>> list(api.Command) >>> list(api.Command)
['user_add'] ['user_add']
>>> api.Command.user_add() # Will call user_add.run() >>> api.Command.user_add(version=u'2.88') # Will call user_add.run()
{'result': 'Added the user!'} {'result': 'Added the user!'}
And third, ``user_add`` can be accessed as an attribute on the ``user`` And third, ``user_add`` can be accessed as an attribute on the ``user``
@@ -1336,7 +1336,7 @@ class Method(Attribute, Command):
['user'] ['user']
>>> list(api.Object.user.methods) >>> list(api.Object.user.methods)
['add'] ['add']
>>> api.Object.user.methods.add() # Will call user_add.run() >>> api.Object.user.methods.add(version=u'2.88') # Will call user_add.run()
{'result': 'Added the user!'} {'result': 'Added the user!'}
The `Attribute` base class implements the naming convention for the The `Attribute` base class implements the naming convention for the

View File

@@ -51,7 +51,7 @@ class AutomountTest(XMLRPC_test):
mock_ui = MockTextui() mock_ui = MockTextui()
command = api.Command['automountlocation_tofiles'] command = api.Command['automountlocation_tofiles']
command.output_for_cli(mock_ui, res, self.locname) command.output_for_cli(mock_ui, res, self.locname, version=u'2.88')
expected_output = self.tofiles_output expected_output = self.tofiles_output
assert_deepequal(expected_output, u'\n'.join(mock_ui)) assert_deepequal(expected_output, u'\n'.join(mock_ui))
@@ -87,7 +87,8 @@ class AutomountTest(XMLRPC_test):
# Feed the files to automountlocation_import & check # Feed the files to automountlocation_import & check
master_file = u'%s/auto.master' % conf_directory master_file = u'%s/auto.master' % conf_directory
automountlocation_import = api.Command['automountlocation_import'] automountlocation_import = api.Command['automountlocation_import']
res = automountlocation_import(self.locname, master_file) res = automountlocation_import(self.locname, master_file,
version=u'2.88')
assert_deepequal(dict( assert_deepequal(dict(
result=dict( result=dict(
keys=lambda k: k, keys=lambda k: k,
@@ -233,7 +234,8 @@ class test_automount(AutomountTest):
""" """
Test the `automountlocation_tofiles` command. Test the `automountlocation_tofiles` command.
""" """
res = api.Command['automountlocation_tofiles'](self.locname) res = api.Command['automountlocation_tofiles'](self.locname,
version=u'2.88')
assert_deepequal(dict( assert_deepequal(dict(
result=dict( result=dict(
keys={'auto.direct': ()}, keys={'auto.direct': ()},