mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
1
Makefile
1
Makefile
@@ -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
|
||||
|
||||
4
VERSION
4
VERSION
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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
51
makeapi
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user