plugable: support plugin versioning

Allow multiple incompatible versions of a plugin using the same name. The
current plugins are assumed to be version '1'.

The unique identifier of plugins was changed from plugin name to plugin
name and version. By default, the highest version available at build time
is used. If the plugin is an unknown remote plugin, version of '1' is used
by default.

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

Reviewed-By: David Kupka <dkupka@redhat.com>
This commit is contained in:
Jan Cholasta
2016-06-21 12:07:21 +02:00
parent 79d1f58335
commit 4284d4fb4d
11 changed files with 1056 additions and 448 deletions

1378
API.txt

File diff suppressed because it is too large Load Diff

View File

@@ -178,6 +178,7 @@ version-update: release-update
sed -i -e "s:__NUM_VERSION__:$(IPA_NUM_VERSION):" ipapython/version.py
sed -i -e "s:__VENDOR_VERSION__:$(IPA_VENDOR_VERSION):" ipapython/version.py
sed -i -e "s:__API_VERSION__:$(IPA_API_VERSION_MAJOR).$(IPA_API_VERSION_MINOR):" ipapython/version.py
grep -Po '(?<=default: ).*' API.txt | sed -n -i -e "/__DEFAULT_PLUGINS__/!{p;b};r /dev/stdin" ipapython/version.py
touch -r ipapython/version.py.in ipapython/version.py
sed -e s/__VERSION__/$(IPA_VERSION)/ daemons/ipa-version.h.in \
> daemons/ipa-version.h

View File

@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
# #
########################################################
IPA_API_VERSION_MAJOR=2
IPA_API_VERSION_MINOR=200
# Last change: automember: TODO
IPA_API_VERSION_MINOR=201
# Last change: plugable: support plugin versioning

View File

@@ -153,6 +153,8 @@ class _SchemaPlugin(object):
def __init__(self, name):
self.name = name
self.version = '1'
self.full_name = '{}/{}'.format(self.name, self.version)
self.__class = None
def _create_default_from(self, api, name, keys):

View File

@@ -728,6 +728,8 @@ class help(frontend.Local):
# build help topics
for c in self.api.Command():
if c is not self.api.Command[c.name]:
continue
if c.NO_CLI:
continue
@@ -792,6 +794,8 @@ class help(frontend.Local):
elif name == "commands":
mcl = 0
for cmd in self.Command():
if cmd is not self.Command[cmd.name]:
continue
if cmd.NO_CLI:
continue
mcl = max(mcl, len(cmd.name))

View File

@@ -1258,6 +1258,8 @@ class Object(HasParam):
namespace = self.api[name]
assert type(namespace) is APINameSpace
for plugin in namespace(): # Equivalent to dict.itervalues()
if plugin is not namespace[plugin.name]:
continue
if plugin.obj_name == self.name:
yield plugin
@@ -1328,10 +1330,16 @@ class Attribute(Plugin):
In practice the `Attribute` class is not used directly, but rather is
only the base class for the `Method` class. Also see the `Object` class.
"""
obj_version = '1'
@property
def obj_name(self):
return self.name.partition('_')[0]
@property
def obj_full_name(self):
return self.obj.full_name
@property
def attr_name(self):
prefix = '{}_'.format(self.obj_name)
@@ -1340,7 +1348,7 @@ class Attribute(Plugin):
@property
def obj(self):
return self.api.Object[self.obj_name]
return self.api.Object[self.obj_name, self.obj_version]
class Method(Attribute, Command):

View File

@@ -25,6 +25,7 @@ you are unfamiliar with this Python feature, see
http://docs.python.org/ref/sequence-types.html
"""
from distutils.version import LooseVersion
import operator
import sys
import threading
@@ -47,7 +48,7 @@ from ipapython.ipa_log_manager import (
log_mgr,
LOGGING_FORMAT_FILE,
LOGGING_FORMAT_STDERR)
from ipapython.version import VERSION, API_VERSION
from ipapython.version import VERSION, API_VERSION, DEFAULT_PLUGINS
if six.PY3:
unicode = str
@@ -125,6 +126,8 @@ class Plugin(ReadOnly):
Base class for all plugins.
"""
version = '1'
def __init__(self, api):
assert api is not None
self.__api = api
@@ -140,6 +143,12 @@ class Plugin(ReadOnly):
# you know nothing, pylint
name = classproperty(__name_getter)
@classmethod
def __full_name_getter(cls):
return '{}/{}'.format(cls.name, cls.version)
full_name = classproperty(__full_name_getter)
@classmethod
def __bases_getter(cls):
return cls.__bases__
@@ -278,6 +287,7 @@ class APINameSpace(collections.Mapping):
if self.__plugins is not None and self.__plugins_by_key is not None:
return
default_map = self.__api._API__default_map
plugins = set()
key_dict = self.__plugins_by_key = {}
@@ -286,9 +296,12 @@ class APINameSpace(collections.Mapping):
continue
plugins.add(plugin)
key_dict[plugin] = plugin
key_dict[plugin.name] = plugin
key_dict[plugin.name, plugin.version] = plugin
key_dict[plugin.full_name] = plugin
if plugin.version == default_map.get(plugin.name, '1'):
key_dict[plugin.name] = plugin
self.__plugins = sorted(plugins, key=operator.attrgetter('name'))
self.__plugins = sorted(plugins, key=operator.attrgetter('full_name'))
def __len__(self):
self.__enumerate()
@@ -326,6 +339,7 @@ class API(ReadOnly):
super(API, self).__init__()
self.__plugins = set()
self.__plugins_by_key = {}
self.__default_map = {}
self.__instances = {}
self.__next = {}
self.__done = set()
@@ -645,7 +659,7 @@ class API(ReadOnly):
)
# Check override:
prev = self.__plugins_by_key.get(plugin.name)
prev = self.__plugins_by_key.get(plugin.full_name)
if prev:
if not override:
# Must use override=True to override:
@@ -668,7 +682,7 @@ class API(ReadOnly):
# The plugin is okay, add to sub_d:
self.__plugins.add(plugin)
self.__plugins_by_key[plugin.name] = plugin
self.__plugins_by_key[plugin.full_name] = plugin
def finalize(self):
"""
@@ -680,6 +694,22 @@ class API(ReadOnly):
self.__doing('finalize')
self.__do_if_not_done('load_plugins')
for plugin in self.__plugins:
if not self.env.validate_api:
if plugin.full_name not in DEFAULT_PLUGINS:
continue
else:
try:
default_version = self.__default_map[plugin.name]
except KeyError:
pass
else:
version = LooseVersion(plugin.version)
default_version = LooseVersion(default_version)
if version < default_version:
continue
self.__default_map[plugin.name] = plugin.version
production_mode = self.is_production_mode()
for base in self.bases:

View File

@@ -46,3 +46,8 @@ NUM_VERSION=__NUM_VERSION__
# The version of the API.
API_VERSION=u'__API_VERSION__'
DEFAULT_PLUGINS = frozenset(l.strip() for l in """
__DEFAULT_PLUGINS__
""".strip().splitlines())

View File

@@ -82,6 +82,7 @@ class json_metadata(Command):
elif objname == "all":
objects = dict(
(o.name, json_serialize(o)) for o in self.api.Object()
if o is self.api.Object[o.name]
)
empty = False
except KeyError:
@@ -96,6 +97,7 @@ class json_metadata(Command):
elif methodname == "all":
methods = dict(
(m.name, json_serialize(m)) for m in self.api.Method()
if m is self.api.Method[m.name]
)
empty = False
except KeyError:
@@ -109,6 +111,7 @@ class json_metadata(Command):
elif cmdname == "all":
commands = dict(
(c.name, json_serialize(c)) for c in self.api.Command()
if c is self.api.Command[c.name]
)
empty = False
except KeyError:
@@ -117,12 +120,15 @@ class json_metadata(Command):
if empty:
objects = dict(
(o.name, json_serialize(o)) for o in self.api.Object()
if o is self.api.Object[o.name]
)
methods = dict(
(m.name, json_serialize(m)) for m in self.api.Method()
if m is self.api.Method[m.name]
)
commands = dict(
(c.name, json_serialize(c)) for c in self.api.Command()
if c is self.api.Command[c.name]
)
retval = dict([

View File

@@ -673,7 +673,8 @@ class xmlserver(KerberosWSGIExecutioner):
"""list methods for XML-RPC introspection"""
if params:
raise errors.ZeroArgumentError(name='system.listMethods')
return (tuple(unicode(cmd.name) for cmd in self.Command()) +
return (tuple(unicode(cmd.name) for cmd in self.Command()
if cmd is self.Command[cmd.name]) +
tuple(unicode(name) for name in self._system_commands))
def _get_method_name(self, name, *params):

51
makeapi
View File

@@ -26,6 +26,7 @@
from __future__ import print_function
import importlib
import itertools
import sys
import os
import re
@@ -246,7 +247,7 @@ def make_api():
"""
fd = open(API_FILE, 'w')
for cmd in api.Command():
fd.write('command: %s\n' % cmd.name)
fd.write('command: %s\n' % cmd.full_name)
fd.write('args: %d,%d,%d\n' % (len(cmd.args), len(cmd.options), len(cmd.output)))
for a in cmd.args():
fd.write('arg: %s\n' % param_repr(a))
@@ -254,6 +255,14 @@ def make_api():
fd.write('option: %s\n' % param_repr(o))
for o in sorted(cmd.output(), key=operator.attrgetter('name')):
fd.write('output: %s\n' % param_repr(o))
for plugin in sorted(itertools.chain(api.Command(), api.Object()),
key=operator.attrgetter('full_name')):
try:
default_plugin = api.Command[plugin.name]
except KeyError:
default_plugin = api.Object[plugin.name]
if plugin is default_plugin:
fd.write('default: %s\n' % plugin.full_name)
for name, version in sorted(
capabilities.items(), key=operator.itemgetter(1, 0)):
fd.write('capability: %s %s\n' % (name, version))
@@ -335,6 +344,7 @@ def validate_api():
# First run through the file and compare it to the API
existing_cmds = []
existing_capabilities = set()
existing_defaults = set()
cmd = None
for line in lines:
line = line.strip()
@@ -417,6 +427,33 @@ def validate_api():
output = find_name(line)
print("Option '%s' in command '%s' in API file not found" % (output, name))
rval |= API_FILE_DIFFERENCE
if line.startswith('default:'):
default = line.replace('default: ', '')
existing_defaults.add(default)
default_name = None
for namespace in (api.Command, api.Object):
try:
default_name = namespace[default].name
except KeyError:
pass
else:
break
else:
print("Plugin %s in API file, not in ipalib" % default)
rval |= API_FILE_DIFFERENCE
if default_name is not None:
try:
expected_default = namespace[default_name].full_name
except KeyError:
print("Default version of plugin %s in API file not "
"found" % default_name)
rval |= API_FILE_DIFFERENCE
else:
if default != expected_default:
print("Default version of plugin %s in API file "
"doesn't match. Got %s, expected %s." %
(default_name, default, expected_default))
rval |= API_FILE_DIFFERENCE
if line.startswith('capability:'):
cap, version = line.replace('capability: ', '').split(' ', 1)
existing_capabilities.add(cap)
@@ -440,10 +477,18 @@ def validate_api():
# Now look for new commands not in the current API
for cmd in api.Command():
if cmd.name not in existing_cmds:
print("Command %s in ipalib, not in API" % cmd.name)
if cmd.full_name not in existing_cmds:
print("Command %s in ipalib, not in API" % cmd.full_name)
rval |= API_NEW_COMMAND
for namespace in (api.Command, api.Object):
for plugin in namespace():
if plugin.name in namespace and namespace[plugin.name] is cmd:
if plugin.full_name not in existing_defaults:
print("Default version of command %s in ipalib, not in "
"API" % plugin.name)
rval |= API_FILE_DIFFERENCE
for cap in capabilities:
if cap not in existing_capabilities:
print("Capability %s in ipalib, not in API" % cap)