blob: 9a23d895edaa209de54bb8f4b3807084e8254ed8 [file] [log] [blame]
#
# -*- coding: utf-8 -*-
#
# This file is part of reclass (http://github.com/madduck/reclass)
#
# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
# Released under the terms of the Artistic Licence 2.0
#
import copy
import time
import re
import fnmatch
import shlex
import string
import sys
import yaml
from six import iteritems
from reclass.settings import Settings
from reclass.output.yaml_outputter import ExplicitDumper
from reclass.datatypes import Entity, Classes, Parameters, Exports
from reclass.errors import MappingFormatError, ClassNotFound, InvQueryClassNotFound, InvQueryError, InterpolationError
try:
basestring
except NameError:
basestring = str
class Core(object):
def __init__(self, storage, class_mappings, settings, input_data=None):
self._storage = storage
self._class_mappings = class_mappings
self._settings = settings
self._input_data = input_data
if self._settings.ignore_class_notfound:
self._cnf_r = re.compile('|'.join([x for x in self._settings.ignore_class_notfound_regexp]))
@staticmethod
def _get_timestamp():
return time.strftime('%c')
@staticmethod
def _match_regexp(key, nodename):
return re.search(key, nodename)
@staticmethod
def _match_glob(key, nodename):
return fnmatch.fnmatchcase(nodename, key)
@staticmethod
def _shlex_split(instr):
lexer = shlex.shlex(instr, posix=True)
lexer.whitespace_split = True
lexer.commenters = ''
regexp = False
if instr[0] == '/':
lexer.quotes += '/'
lexer.escapedquotes += '/'
regexp = True
try:
key = lexer.get_token()
except ValueError as e:
raise MappingFormatError('Error in mapping "{0}": missing closing '
'quote (or slash)'.format(instr))
if regexp:
key = '/{0}/'.format(key)
return key, list(lexer)
def _get_class_mappings_entity(self, nodename):
if not self._class_mappings:
return Entity(self._settings, name='empty (class mappings)')
c = Classes()
for mapping in self._class_mappings:
matched = False
key, klasses = Core._shlex_split(mapping)
if key[0] == ('/'):
matched = Core._match_regexp(key[1:-1], nodename)
if matched:
for klass in klasses:
c.append_if_new(matched.expand(klass))
else:
if Core._match_glob(key, nodename):
for klass in klasses:
c.append_if_new(klass)
return Entity(self._settings, classes=c,
name='class mappings for node {0}'.format(nodename))
def _get_input_data_entity(self):
if not self._input_data:
return Entity(self._settings, name='empty (input data)')
p = Parameters(self._input_data, self._settings)
return Entity(self._settings, parameters=p, name='input data')
def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, environment=None):
if seen is None:
seen = {}
if environment is None:
environment = self._settings.default_environment
if merge_base is None:
merge_base = Entity(self._settings, name='empty (@{0})'.format(nodename))
for klass in entity.classes.as_list():
if klass not in seen:
try:
class_entity = self._storage.get_class(klass, environment, self._settings)
except ClassNotFound as e:
if self._settings.ignore_class_notfound:
if self._cnf_r.match(klass):
if self._settings.ignore_class_notfound_warning:
# TODO, add logging handler
print >>sys.stderr, "[WARNING] Reclass class not found: '%s'. Skipped!" % klass
continue
e.nodename = nodename
e.uri = entity.uri
raise
descent = self._recurse_entity(class_entity, seen=seen,
nodename=nodename, environment=environment)
# on every iteration, we merge the result of the recursive
# descent into what we have so far…
merge_base.merge(descent)
seen[klass] = True
# … and finally, we merge what we have at this level into the
# result of the iteration, so that elements at the current level
# overwrite stuff defined by parents
merge_base.merge(entity)
return merge_base
def _get_automatic_parameters(self, nodename, environment):
if self._settings.automatic_parameters:
return Parameters({ '_reclass_': { 'name': { 'full': nodename, 'short': str.split(nodename, '.')[0] },
'environment': environment } }, self._settings, '__auto__')
else:
return Parameters({}, self._settings, '')
def _get_inventory(self, all_envs, environment, queries):
inventory = {}
for nodename in self._storage.enumerate_nodes():
try:
node_base = self._storage.get_node(nodename, self._settings)
if node_base.environment == None:
node_base.environment = self._settings.default_environment
except yaml.scanner.ScannerError as e:
if self._settings.inventory_ignore_failed_node:
continue
else:
raise
if all_envs or node_base.environment == environment:
try:
node = self._node_entity(nodename)
except ClassNotFound as e:
raise InvQueryClassNotFound(e)
if queries is None:
try:
node.interpolate_exports()
except InterpolationError as e:
e.nodename = nodename
else:
node.initialise_interpolation()
for p, q in queries:
try:
node.interpolate_single_export(q)
except InterpolationError as e:
e.nodename = nodename
raise InvQueryError(q.contents(), e, context=p, uri=q.uri())
inventory[nodename] = node.exports.as_dict()
return inventory
def _node_entity(self, nodename):
node_entity = self._storage.get_node(nodename, self._settings)
if node_entity.environment == None:
node_entity.environment = self._settings.default_environment
base_entity = Entity(self._settings, name='base')
base_entity.merge(self._get_class_mappings_entity(node_entity.name))
base_entity.merge(self._get_input_data_entity())
base_entity.merge_parameters(self._get_automatic_parameters(nodename, node_entity.environment))
seen = {}
merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename,
environment=node_entity.environment)
return self._recurse_entity(node_entity, merge_base, seen=seen, nodename=nodename,
environment=node_entity.environment)
def _nodeinfo(self, nodename, inventory):
try:
node = self._node_entity(nodename)
node.initialise_interpolation()
if node.parameters.has_inv_query() and inventory is None:
inventory = self._get_inventory(node.parameters.needs_all_envs(), node.environment, node.parameters.get_inv_queries())
node.interpolate(inventory)
return node
except InterpolationError as e:
e.nodename = nodename
raise
def _nodeinfo_as_dict(self, nodename, entity):
ret = {'__reclass__' : {'node': entity.name, 'name': nodename,
'uri': entity.uri,
'environment': entity.environment,
'timestamp': Core._get_timestamp()
},
}
ret.update(entity.as_dict())
return ret
def nodeinfo(self, nodename):
return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename, None))
def inventory(self):
query_nodes = set()
entities = {}
inventory = self._get_inventory(True, '', None)
for n in self._storage.enumerate_nodes():
entities[n] = self._nodeinfo(n, inventory)
if entities[n].parameters.has_inv_query():
nodes.add(n)
for n in query_nodes:
entities[n] = self._nodeinfo(n, inventory)
nodes = {}
applications = {}
classes = {}
for (f, nodeinfo) in iteritems(entities):
d = nodes[f] = self._nodeinfo_as_dict(f, nodeinfo)
for a in d['applications']:
if a in applications:
applications[a].append(f)
else:
applications[a] = [f]
for c in d['classes']:
if c in classes:
classes[c].append(f)
else:
classes[c] = [f]
return {'__reclass__' : {'timestamp': Core._get_timestamp()},
'nodes': nodes,
'classes': classes,
'applications': applications
}