Files
freeipa/ipaclient/remote_plugins/schema.py
T

617 lines
18 KiB
Python
Raw Normal View History

2016-06-02 10:12:26 +02:00
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
import collections
import contextlib
2016-06-21 14:42:04 +02:00
import errno
import fcntl
import io
2016-06-21 14:42:04 +02:00
import json
import os
2016-06-02 10:12:26 +02:00
import sys
import types
2016-06-21 14:42:04 +02:00
import zipfile
2016-06-02 10:12:26 +02:00
import six
2016-06-30 15:51:29 +02:00
from ipaclient.frontend import ClientCommand, ClientMethod
2016-06-21 14:42:04 +02:00
from ipalib import errors, parameters, plugable
from ipalib.errors import SchemaUpToDate
2016-06-30 15:51:29 +02:00
from ipalib.frontend import Object
2016-06-02 10:12:26 +02:00
from ipalib.output import Output
2016-06-22 13:27:25 +02:00
from ipalib.parameters import DefaultFrom, Flag, Password, Str
from ipapython.ipautil import fsdecode
2016-06-02 10:12:26 +02:00
from ipapython.dn import DN
from ipapython.dnsutil import DNSName
2016-06-21 14:42:04 +02:00
from ipapython.ipa_log_manager import log_mgr
2016-06-02 10:12:26 +02:00
FORMAT = '1'
2016-07-27 10:54:16 +02:00
2016-06-02 10:12:26 +02:00
if six.PY3:
unicode = str
USER_CACHE_PATH = (
os.environ.get('XDG_CACHE_HOME') or
os.path.join(
os.environ.get(
'HOME',
os.path.expanduser('~')
),
'.cache'
)
)
2016-06-02 10:12:26 +02:00
_TYPES = {
'DN': DN,
'DNSName': DNSName,
2016-06-23 18:23:00 +02:00
'Principal': unicode,
2016-06-02 10:12:26 +02:00
'NoneType': type(None),
'Sequence': collections.Sequence,
'bool': bool,
'dict': dict,
'int': int,
'list': list,
'tuple': tuple,
'unicode': unicode,
}
_PARAMS = {
'Decimal': parameters.Decimal,
'DN': parameters.DNParam,
'DNSName': parameters.DNSNameParam,
2016-06-23 18:23:00 +02:00
'Principal': parameters.Principal,
2016-06-02 10:12:26 +02:00
'bool': parameters.Bool,
'bytes': parameters.Bytes,
'datetime': parameters.DateTime,
2016-06-03 07:31:38 +02:00
'dict': parameters.Dict,
2016-06-02 10:12:26 +02:00
'int': parameters.Int,
'str': parameters.Str,
}
2016-06-21 14:42:04 +02:00
logger = log_mgr.get_logger(__name__)
2016-06-02 10:12:26 +02:00
2016-06-30 15:51:29 +02:00
class _SchemaCommand(ClientCommand):
pass
2016-06-02 10:12:26 +02:00
2016-06-30 15:51:29 +02:00
class _SchemaMethod(ClientMethod):
2016-06-21 12:43:54 +02:00
@property
def obj_name(self):
return self.api.Object[self.obj_full_name].name
@property
def obj_version(self):
return self.api.Object[self.obj_full_name].version
2016-06-16 13:21:57 +02:00
2016-06-22 13:27:25 +02:00
class _SchemaObject(Object):
2016-06-02 10:12:26 +02:00
pass
2016-06-22 13:27:25 +02:00
class _SchemaPlugin(object):
bases = None
schema_key = None
2016-06-02 10:12:26 +02:00
def __init__(self, schema, full_name):
2016-06-21 12:43:54 +02:00
self.name, _slash, self.version = full_name.partition('/')
self.full_name = full_name
self._schema = schema
self._class = None
@property
def doc(self):
if self._class is not None:
return self._class.doc
else:
schema = self._schema[self.schema_key][self.full_name]
try:
return schema['doc']
except KeyError:
return None
@property
def summary(self):
if self._class is not None:
return self._class.summary
else:
2016-08-04 16:14:33 +02:00
halp = self._schema[self.schema_key].get_help(self.full_name)
try:
2016-08-04 16:14:33 +02:00
return halp['summary']
except KeyError:
return u'<%s>' % self.full_name
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
def _create_default_from(self, api, name, keys):
2016-06-21 12:43:54 +02:00
cmd_name = self.full_name
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
def get_default(*args):
kw = dict(zip(keys, args))
result = api.Command.command_defaults(
unicode(cmd_name),
params=[unicode(name)],
kw=kw,
)['result']
return result.get(name)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
if keys:
def callback(*args):
return get_default(*args)
2016-06-02 10:12:26 +02:00
else:
2016-06-22 13:27:25 +02:00
def callback():
return get_default()
2016-06-02 10:12:26 +02:00
2016-06-21 12:43:54 +02:00
callback.__name__ = '{0}_{1}_default'.format(self.name, name)
2016-06-22 15:15:32 +02:00
2016-06-22 13:27:25 +02:00
return DefaultFrom(callback, *keys)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
def _create_param(self, api, schema):
name = str(schema['name'])
type_name = str(schema['type'])
sensitive = schema.get('sensitive', False)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
if type_name == 'str' and sensitive:
cls = Password
sensitive = False
elif (type_name == 'bool' and
'default' in schema and schema['default'][0] == u'False' and
not schema.get('alwaysask', False)):
2016-06-22 13:27:25 +02:00
cls = Flag
del schema['default']
else:
try:
cls = _PARAMS[type_name]
except KeyError:
cls = Str
kwargs = {}
default = None
for key, value in schema.items():
if key in ('alwaysask',
'doc',
'label',
'multivalue',
'no_convert',
'option_group',
'required'):
kwargs[key] = value
elif key in ('cli_metavar',
'cli_name'):
kwargs[key] = str(value)
2016-08-08 13:09:39 +02:00
elif key == 'confirm':
2016-06-22 13:27:25 +02:00
kwargs[key] = value
elif key == 'default':
default = value
elif key == 'default_from_param':
keys = tuple(str(k) for k in value)
kwargs['default_from'] = (
self._create_default_from(api, name, keys))
elif key in ('exclude',
'include'):
kwargs[key] = tuple(str(v) for v in value)
if default is not None:
tmp = cls(name, **dict(kwargs, no_convert=False))
if tmp.multivalue:
default = tuple(tmp._convert_scalar(d) for d in default)
else:
default = tmp._convert_scalar(default[0])
kwargs['default'] = default
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
if 'default' in kwargs or 'default_from' in kwargs:
kwargs['autofill'] = not kwargs.pop('alwaysask', False)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
param = cls(name, **kwargs)
2016-06-22 13:27:25 +02:00
if sensitive:
object.__setattr__(param, 'password', True)
2016-06-22 13:27:25 +02:00
return param
def _create_class(self, api, schema):
class_dict = {}
2016-06-21 12:43:54 +02:00
class_dict['name'] = str(schema['name'])
class_dict['version'] = str(schema['version'])
class_dict['full_name'] = str(schema['full_name'])
2016-06-22 13:27:25 +02:00
if 'doc' in schema:
class_dict['doc'] = schema['doc']
if 'topic_topic' in schema:
2016-06-21 12:43:54 +02:00
class_dict['topic'] = str(schema['topic_topic']).partition('/')[0]
2016-06-16 13:21:17 +02:00
else:
2016-06-22 13:27:25 +02:00
class_dict['topic'] = None
class_dict['takes_params'] = tuple(self._create_param(api, s)
for s in schema.get('params', []))
return self.name, self.bases, class_dict
def __call__(self, api):
if self._class is None:
schema = self._schema[self.schema_key][self.full_name]
2016-06-22 13:27:25 +02:00
name, bases, class_dict = self._create_class(api, schema)
self._class = type(name, bases, class_dict)
return self._class(api)
2016-06-22 13:27:25 +02:00
class _SchemaCommandPlugin(_SchemaPlugin):
bases = (_SchemaCommand,)
schema_key = 'commands'
@property
def topic(self):
if self._class is not None:
return self._class.topic
else:
2016-08-04 16:14:33 +02:00
halp = self._schema[self.schema_key].get_help(self.full_name)
try:
2016-08-04 16:14:33 +02:00
return str(halp['topic_topic']).partition('/')[0]
except KeyError:
return None
@property
def NO_CLI(self):
if self._class is not None:
return self._class.NO_CLI
else:
2016-08-04 16:14:33 +02:00
halp = self._schema[self.schema_key].get_help(self.full_name)
return 'cli' in halp.get('exclude', [])
2016-06-22 13:27:25 +02:00
def _create_output(self, api, schema):
if schema.get('multivalue', False):
type_type = (tuple, list)
if not schema.get('required', True):
type_type = type_type + (type(None),)
else:
try:
type_type = _TYPES[schema['type']]
except KeyError:
type_type = None
else:
if not schema.get('required', True):
type_type = (type_type, type(None))
kwargs = {}
kwargs['type'] = type_type
2016-06-16 13:21:17 +02:00
2016-06-22 13:27:25 +02:00
if 'doc' in schema:
kwargs['doc'] = schema['doc']
2016-06-16 13:21:17 +02:00
2016-06-22 13:27:25 +02:00
if schema.get('no_display', False):
kwargs['flags'] = ('no_display',)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
return Output(str(schema['name']), **kwargs)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
def _create_class(self, api, schema):
name, bases, class_dict = (
super(_SchemaCommandPlugin, self)._create_class(api, schema))
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
if 'obj_class' in schema or 'attr_name' in schema:
bases = (_SchemaMethod,)
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
if 'obj_class' in schema:
2016-06-21 12:43:54 +02:00
class_dict['obj_full_name'] = str(schema['obj_class'])
2016-06-22 13:27:25 +02:00
if 'attr_name' in schema:
class_dict['attr_name'] = str(schema['attr_name'])
if 'exclude' in schema and u'cli' in schema['exclude']:
class_dict['NO_CLI'] = True
2016-06-02 10:12:26 +02:00
2016-06-22 13:27:25 +02:00
args = set(str(s['name']) for s in schema['params']
if s.get('positional', s.get('required', True)))
class_dict['takes_args'] = tuple(
p for p in class_dict['takes_params'] if p.name in args)
class_dict['takes_options'] = tuple(
p for p in class_dict['takes_params'] if p.name not in args)
del class_dict['takes_params']
class_dict['has_output'] = tuple(
self._create_output(api, s) for s in schema['output'])
return name, bases, class_dict
class _SchemaObjectPlugin(_SchemaPlugin):
bases = (_SchemaObject,)
schema_key = 'classes'
2016-06-02 10:12:26 +02:00
2016-06-21 14:42:04 +02:00
class _SchemaNameSpace(collections.Mapping):
def __init__(self, schema, name):
self.name = name
self._schema = schema
def __getitem__(self, key):
2016-07-27 10:46:40 +02:00
try:
return self._schema.read_namespace_member(self.name, key)
except KeyError:
raise KeyError(key)
2016-06-21 14:42:04 +02:00
def __iter__(self):
for key in self._schema.iter_namespace(self.name):
yield key
def __len__(self):
return len(list(self._schema.iter_namespace(self.name)))
2016-08-04 16:14:33 +02:00
def get_help(self, key):
try:
return self._schema.get_help(self.name, key)
except KeyError:
raise KeyError(key)
2016-06-21 14:42:04 +02:00
2016-06-30 15:51:29 +02:00
class NotAvailable(Exception):
pass
2016-06-21 14:42:04 +02:00
class Schema(object):
"""
Store and provide schema for commands and topics
2016-06-22 13:27:25 +02:00
2016-06-21 14:42:04 +02:00
Create api instance
>>> from ipalib import api
>>> api.bootstrap(context='cli')
>>> api.finalize()
Get schema object
>>> m = Schema(api)
From now on we can access schema for commands stored in cache
>>> m['commands'][u'ping'][u'doc']
u'Ping a remote server.'
>>> m['topics'][u'ping'][u'doc']
u'Ping the remote IPA server to ...'
"""
namespaces = {'classes', 'commands', 'topics'}
_DIR = os.path.join(USER_CACHE_PATH, 'ipa', 'schema', FORMAT)
2016-06-21 14:42:04 +02:00
def __init__(self, client, fingerprint=None):
2016-07-27 10:46:40 +02:00
self._dict = {}
self._namespaces = {}
self._help = None
self._file = six.BytesIO()
2016-06-21 14:42:04 +02:00
2016-07-27 10:46:40 +02:00
for ns in self.namespaces:
self._dict[ns] = {}
self._namespaces[ns] = _SchemaNameSpace(self, ns)
2016-06-21 14:42:04 +02:00
ttl = None
read_failed = False
2016-07-27 10:46:40 +02:00
if fingerprint is not None:
2016-07-27 10:46:40 +02:00
try:
self._read_schema(fingerprint)
except Exception as e:
# Failed to read the schema from cache. There may be a lot of
# causes and not much we can do about it. Just ensure we will
# ignore the cache and fetch the schema from server.
logger.warning("Failed to read schema: {}".format(e))
fingerprint = None
read_failed = True
if fingerprint is None:
fingerprint, ttl = self._fetch(client, ignore_cache=read_failed)
try:
self._write_schema(fingerprint)
except Exception as e:
logger.warning("Failed to write schema: {}".format(e))
self.fingerprint = fingerprint
self.ttl = ttl
2016-06-21 14:42:04 +02:00
@contextlib.contextmanager
def _open(self, filename, mode):
2016-07-27 10:46:40 +02:00
path = os.path.join(self._DIR, filename)
with io.open(path, mode) as f:
if mode.startswith('r'):
fcntl.flock(f, fcntl.LOCK_SH)
else:
fcntl.flock(f, fcntl.LOCK_EX)
try:
yield f
finally:
fcntl.flock(f, fcntl.LOCK_UN)
2016-07-27 10:46:40 +02:00
def _fetch(self, client, ignore_cache=False):
2016-06-30 15:51:29 +02:00
if not client.isconnected():
client.connect(verbose=False)
2016-06-21 14:42:04 +02:00
fps = []
if not ignore_cache:
try:
fps = [fsdecode(f) for f in os.listdir(self._DIR)]
except EnvironmentError:
pass
2016-07-27 10:46:40 +02:00
2016-06-21 14:42:04 +02:00
kwargs = {u'version': u'2.170'}
if fps:
kwargs[u'known_fingerprints'] = fps
2016-06-22 13:27:25 +02:00
try:
2016-06-21 14:42:04 +02:00
schema = client.forward(u'schema', **kwargs)['result']
2016-06-30 15:51:29 +02:00
except errors.CommandError:
raise NotAvailable()
try:
2016-06-21 14:42:04 +02:00
fp = schema['fingerprint']
ttl = schema.pop('ttl')
schema.pop('version')
2016-06-22 13:27:25 +02:00
2016-07-27 10:46:40 +02:00
for key, value in schema.items():
if key in self.namespaces:
value = {m['full_name']: m for m in value}
self._dict[key] = value
except KeyError as e:
logger.warning("Failed to fetch schema: %s", e)
raise NotAvailable()
2016-06-21 14:42:04 +02:00
return (fp, ttl,)
2016-07-27 10:46:40 +02:00
def _read_schema(self, fingerprint):
self._file.truncate(0)
with self._open(fingerprint, 'rb') as f:
self._file.write(f.read())
with zipfile.ZipFile(self._file, 'r') as schema:
2016-07-27 10:46:40 +02:00
for name in schema.namelist():
ns, _slash, key = name.partition('/')
if ns in self.namespaces:
2016-08-04 16:14:33 +02:00
self._dict[ns][key] = None
2016-06-21 14:42:04 +02:00
def __getitem__(self, key):
2016-07-27 10:46:40 +02:00
try:
return self._namespaces[key]
except KeyError:
return self._dict[key]
2016-06-21 14:42:04 +02:00
def _generate_help(self, schema):
halp = {}
for namespace in ('commands', 'topics'):
halp[namespace] = {}
for member_schema in schema[namespace].values():
member_full_name = member_schema['full_name']
topic = halp[namespace].setdefault(member_full_name, {})
topic['name'] = member_schema['name']
if 'doc' in member_schema:
topic['summary'] = (
member_schema['doc'].split('\n\n', 1)[0].strip())
if 'topic_topic' in member_schema:
topic['topic_topic'] = member_schema['topic_topic']
if 'exclude' in member_schema:
topic['exclude'] = member_schema['exclude']
return halp
def _write_schema(self, fingerprint):
2016-07-27 10:46:40 +02:00
try:
os.makedirs(self._DIR)
except EnvironmentError as e:
if e.errno != errno.EEXIST:
raise
2016-07-27 10:46:40 +02:00
self._file.truncate(0)
with zipfile.ZipFile(self._file, 'w', zipfile.ZIP_DEFLATED) as schema:
2016-07-27 10:46:40 +02:00
for key, value in self._dict.items():
if key in self.namespaces:
ns = value
for member in ns:
path = '{}/{}'.format(key, member)
schema.writestr(path,
json.dumps(ns[member]).encode('utf-8'))
2016-07-27 10:46:40 +02:00
else:
schema.writestr(key, json.dumps(value).encode('utf-8'))
2016-07-27 10:46:40 +02:00
schema.writestr(
'_help',
json.dumps(self._generate_help(self._dict)).encode('utf-8')
)
2016-06-21 14:42:04 +02:00
self._file.seek(0)
with self._open(fingerprint, 'wb') as f:
f.truncate(0)
f.write(self._file.read())
2016-06-21 14:42:04 +02:00
def _read(self, path):
with zipfile.ZipFile(self._file, 'r') as zf:
return json.loads(zf.read(path).decode('utf-8'))
2016-06-21 14:42:04 +02:00
def read_namespace_member(self, namespace, member):
2016-07-27 10:46:40 +02:00
value = self._dict[namespace][member]
2016-08-04 16:14:33 +02:00
if value is None:
2016-07-27 10:46:40 +02:00
path = '{}/{}'.format(namespace, member)
2016-08-04 16:14:33 +02:00
value = self._dict[namespace][member] = self._read(path)
2016-07-27 10:46:40 +02:00
return value
2016-06-21 14:42:04 +02:00
def iter_namespace(self, namespace):
2016-07-27 10:46:40 +02:00
return iter(self._dict[namespace])
2016-06-21 14:42:04 +02:00
2016-08-04 16:14:33 +02:00
def get_help(self, namespace, member):
if not self._help:
self._help = self._read('_help')
2016-08-04 16:14:33 +02:00
return self._help[namespace][member]
2016-06-22 13:27:25 +02:00
def get_package(server_info, client):
NO_FINGERPRINT = object()
fingerprint = NO_FINGERPRINT
if server_info.is_valid():
fingerprint = server_info.get('fingerprint', fingerprint)
if fingerprint is not None:
try:
try:
if fingerprint is NO_FINGERPRINT:
schema = Schema(client)
else:
schema = Schema(client, fingerprint)
except SchemaUpToDate as e:
schema = Schema(client, e.fingerprint)
except NotAvailable:
fingerprint = None
ttl = None
except SchemaUpToDate as e:
fingerprint = e.fingerprint
ttl = e.ttl
else:
fingerprint = schema.fingerprint
ttl = schema.ttl
server_info['fingerprint'] = fingerprint
server_info.update_validity(ttl)
if fingerprint is None:
raise NotAvailable()
2016-06-22 13:27:25 +02:00
fingerprint = str(fingerprint)
2016-06-22 13:27:25 +02:00
package_name = '{}${}'.format(__name__, fingerprint)
package_dir = '{}${}'.format(os.path.splitext(__file__)[0], fingerprint)
2016-06-02 10:12:26 +02:00
try:
return sys.modules[package_name]
except KeyError:
pass
package = types.ModuleType(package_name)
package.__file__ = os.path.join(package_dir, '__init__.py')
2016-06-22 13:27:25 +02:00
package.modules = ['plugins']
2016-06-02 10:12:26 +02:00
sys.modules[package_name] = package
2016-06-22 13:27:25 +02:00
module_name = '.'.join((package_name, 'plugins'))
2016-06-02 10:12:26 +02:00
module = types.ModuleType(module_name)
2016-06-22 13:27:25 +02:00
module.__file__ = os.path.join(package_dir, 'plugins.py')
2016-06-02 10:12:26 +02:00
module.register = plugable.Registry()
2016-07-27 10:46:40 +02:00
for plugin_cls in (_SchemaCommandPlugin, _SchemaObjectPlugin):
for full_name in schema[plugin_cls.schema_key]:
plugin = plugin_cls(schema, str(full_name))
2016-06-22 13:27:25 +02:00
plugin = module.register()(plugin)
2016-06-02 10:12:26 +02:00
sys.modules[module_name] = module
2016-06-21 12:43:54 +02:00
for full_name, topic in six.iteritems(schema['topics']):
name = str(topic['name'])
2016-06-02 10:12:26 +02:00
module_name = '.'.join((package_name, name))
try:
module = sys.modules[module_name]
except KeyError:
module = sys.modules[module_name] = types.ModuleType(module_name)
module.__file__ = os.path.join(package_dir, '{}.py'.format(name))
2016-06-22 13:27:25 +02:00
module.__doc__ = topic.get('doc')
if 'topic_topic' in topic:
2016-06-21 12:43:54 +02:00
module.topic = str(topic['topic_topic']).partition('/')[0]
2016-06-22 13:27:25 +02:00
else:
module.topic = None
2016-06-02 10:12:26 +02:00
return package