cdist-backup/cdist/emulator.py
Evil Ham ba77ea9edc [UX] Add option to enable LogLevel-based coloured output.
This makes it easier for new and experienced users to run cdist with higher
verbosity levels, both to know that things are working as expected and to debug
issues.

Documentation has been modified accordingly and default behaviour is not
changed.
2020-06-01 19:11:58 +02:00

440 lines
18 KiB
Python

# -*- coding: utf-8 -*-
#
# 2011-2015 Nico Schottelius (nico-cdist at schottelius.org)
# 2012-2013 Steven Armstrong (steven-cdist at armstrong.cc)
# 2014 Daniel Heule (hda at sfs.biz)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
#
import argparse
import logging
import os
import sys
import cdist
from cdist import core
from cdist import flock
from cdist.core.manifest import Manifest
class MissingRequiredEnvironmentVariableError(cdist.Error):
def __init__(self, name):
self.name = name
self.message = ("Emulator requires the environment variable %s to be "
"setup" % self.name)
def __str__(self):
return self.message
class DefaultList(list):
"""Helper class to allow default values for optional_multiple parameters.
@see https://groups.google.com/forum/#!msg/comp.lang.python/sAUvkJEDpRc/RnRymrzJVDYJ # noqa
"""
def __copy__(self):
return []
@classmethod
def create(cls, initial=None):
if initial:
return cls(initial.split('\n'))
class Emulator(object):
def __init__(self, argv, stdin=sys.stdin.buffer, env=os.environ):
self.argv = argv
self.stdin = stdin
self.env = env
self.object_id = ''
try:
self.global_path = self.env['__global']
self.target_host = (
self.env['__target_host'],
self.env['__target_hostname'],
self.env['__target_fqdn']
)
# Internal variables
self.object_source = self.env['__cdist_manifest']
self.type_base_path = self.env['__cdist_type_base_path']
self.object_marker = self.env['__cdist_object_marker']
except KeyError as e:
raise MissingRequiredEnvironmentVariableError(e.args[0])
self.object_base_path = os.path.join(self.global_path, "object")
self.typeorder_path = os.path.join(self.global_path, "typeorder")
self.typeorder_dep_path = os.path.join(self.global_path,
Manifest.TYPEORDER_DEP_NAME)
self.order_dep_state_path = os.path.join(self.global_path,
Manifest.ORDER_DEP_STATE_NAME)
self.type_name = os.path.basename(argv[0])
self.cdist_type = core.CdistType(self.type_base_path, self.type_name)
# If set then object alreay exists and this var holds existing
# requirements.
self._existing_reqs = None
self.__init_log()
def run(self):
"""Emulate type commands (i.e. __file and co)"""
self.commandline()
self.init_object()
# locking for parallel execution
with flock.Flock(self.flock_path):
self.setup_object()
self.save_stdin()
self.record_requirements()
self.record_auto_requirements()
self.log.trace("Finished %s %s" % (
self.cdist_object.path, self.parameters))
def __init_log(self):
"""Setup logging facility"""
if '__cdist_log_level' in self.env:
try:
loglevel = self.env['__cdist_log_level']
level = int(loglevel)
except ValueError:
level = logging.WARNING
else:
level = logging.WARNING
try:
logging.root.setLevel(level)
except (ValueError, TypeError):
# if invalid __cdist_log_level value
logging.root.setLevel(logging.WARNING)
colored_log = self.env.get('__cdist_colored_log', 'False')
cdist.log.ColorFormatter.USE_COLORS = colored_log == 'True'
self.log = logging.getLogger(self.target_host[0])
def commandline(self):
"""Parse command line"""
parser = argparse.ArgumentParser(add_help=False,
argument_default=argparse.SUPPRESS)
for parameter in self.cdist_type.required_parameters:
argument = "--" + parameter
parser.add_argument(argument, dest=parameter, action='store',
required=True)
for parameter in self.cdist_type.required_multiple_parameters:
argument = "--" + parameter
parser.add_argument(argument, dest=parameter, action='append',
required=True)
for parameter in self.cdist_type.optional_parameters:
argument = "--" + parameter
default = self.cdist_type.parameter_defaults.get(parameter, None)
parser.add_argument(argument, dest=parameter, action='store',
required=False, default=default)
for parameter in self.cdist_type.optional_multiple_parameters:
argument = "--" + parameter
default = DefaultList.create(
self.cdist_type.parameter_defaults.get(
parameter, None))
parser.add_argument(argument, dest=parameter, action='append',
required=False, default=default)
for parameter in self.cdist_type.boolean_parameters:
argument = "--" + parameter
parser.add_argument(argument, dest=parameter,
action='store_const', const='')
# If not singleton support one positional parameter
if not self.cdist_type.is_singleton:
parser.add_argument("object_id", nargs=1)
# And finally parse/verify parameter
self.args = parser.parse_args(self.argv[1:])
self.log.trace('Args: %s' % self.args)
def init_object(self):
# Initialize object - and ensure it is not in args
if self.cdist_type.is_singleton:
self.object_id = ''
else:
self.object_id = self.args.object_id[0]
del self.args.object_id
# Instantiate the cdist object we are defining
self.cdist_object = core.CdistObject(
self.cdist_type, self.object_base_path, self.object_marker,
self.object_id)
lockfname = ('.' + self.cdist_type.name +
self.object_id + '_' +
self.object_marker + '.lock')
lockfname = lockfname.replace(os.sep, '_')
self.flock_path = os.path.join(self.object_base_path, lockfname)
def _object_params_in_context(self):
''' Get cdist_object parameters dict adopted by context.
Context consists of cdist_type boolean, optional, required,
optional_multiple and required_multiple parameters. If parameter
is multiple parameter then its value is a list.
This adaptation works on cdist_object.parameters which are read from
directory based dict where it is unknown what kind of data is in
file. If there is only one line in the file it is unknown if this
is a value of required/optional parameter or if it is one value of
multiple values parameter.
'''
params = {}
if self.cdist_object.exists:
for param in self.cdist_object.parameters:
value = ('' if param in self.cdist_type.boolean_parameters
else self.cdist_object.parameters[param])
if ((param in self.cdist_type.required_multiple_parameters or
param in self.cdist_type.optional_multiple_parameters) and
not isinstance(value, list)):
value = [value]
params[param] = value
return params
def setup_object(self):
# CDIST_ORDER_DEPENDENCY state
order_dep_on = self._order_dep_on()
order_dep_defined = "CDIST_ORDER_DEPENDENCY" in self.env
if not order_dep_defined and order_dep_on:
self._set_order_dep_state_off()
if order_dep_defined and not order_dep_on:
self._set_order_dep_state_on()
# Create object with given parameters
self.parameters = {}
for key, value in vars(self.args).items():
if value is not None:
self.parameters[key] = value
if self.cdist_object.exists and 'CDIST_OVERRIDE' not in self.env:
# Make existing requirements a set so that we can compare it
# later with new requirements.
self._existing_reqs = set(self.cdist_object.requirements)
obj_params = self._object_params_in_context()
if obj_params != self.parameters:
errmsg = ("Object %s already exists with conflicting "
"parameters:\n%s: %s\n%s: %s" % (
self.cdist_object.name,
" ".join(self.cdist_object.source),
obj_params,
self.object_source,
self.parameters))
raise cdist.Error(errmsg)
else:
if self.cdist_object.exists:
self.log.debug(('Object %s override forced with '
'CDIST_OVERRIDE'), self.cdist_object.name)
self.cdist_object.create(True)
else:
self.cdist_object.create()
self.cdist_object.parameters = self.parameters
# record the created object in typeorder file
with open(self.typeorder_path, 'a') as typeorderfile:
print(self.cdist_object.name, file=typeorderfile)
# record the created object in parent object typeorder file
__object_name = self.env.get('__object_name', None)
depname = self.cdist_object.name
if __object_name:
parent = self.cdist_object.object_from_name(__object_name)
parent.typeorder.append(self.cdist_object.name)
if self._order_dep_on():
self.log.trace(('[ORDER_DEP] Adding %s to typeorder dep'
' for %s'), depname, parent.name)
parent.typeorder_dep.append(depname)
elif self._order_dep_on():
self.log.trace('[ORDER_DEP] Adding %s to global typeorder dep',
depname)
self._add_typeorder_dep(depname)
# Record / Append source
self.cdist_object.source.append(self.object_source)
chunk_size = 65536
def _read_stdin(self):
return self.stdin.read(self.chunk_size)
def save_stdin(self):
"""If something is written to stdin, save it in the object as
$__object/stdin so it can be accessed in manifest and gencode-*
scripts.
"""
if not self.stdin.isatty():
try:
# go directly to file instead of using CdistObject's api
# as that does not support streaming
path = os.path.join(self.cdist_object.absolute_path, 'stdin')
with open(path, 'wb') as fd:
chunk = self._read_stdin()
while chunk:
fd.write(chunk)
chunk = self._read_stdin()
except EnvironmentError as e:
raise cdist.Error('Failed to read from stdin: %s' % e)
def record_requirement(self, requirement):
"""record requirement and return recorded requirement"""
# Raises an error, if object cannot be created
try:
cdist_object = self.cdist_object.object_from_name(requirement)
except core.cdist_type.InvalidTypeError as e:
self.log.error(("%s requires object %s, but type %s does not"
" exist. Defined at %s" % (
self.cdist_object.name,
requirement, e.name, self.object_source)))
raise
except core.cdist_object.MissingObjectIdError:
self.log.error(("%s requires object %s without object id."
" Defined at %s" % (self.cdist_object.name,
requirement,
self.object_source)))
raise
self.log.debug("Recording requirement %s for %s",
requirement, self.cdist_object.name)
# Save the sanitised version, not the user supplied one
# (__file//bar => __file/bar)
# This ensures pattern matching is done against sanitised list
self.cdist_object.requirements.append(cdist_object.name)
return cdist_object.name
def _order_dep_on(self):
return os.path.exists(self.order_dep_state_path)
def _set_order_dep_state_on(self):
self.log.trace('[ORDER_DEP] Setting order dep state on')
with open(self.order_dep_state_path, 'w'):
pass
def _set_order_dep_state_off(self):
self.log.trace('[ORDER_DEP] Setting order dep state off')
# remove order dep state file
try:
os.remove(self.order_dep_state_path)
except FileNotFoundError:
pass
# remove typeorder dep file
try:
os.remove(self.typeorder_dep_path)
except FileNotFoundError:
pass
def _add_typeorder_dep(self, name):
with open(self.typeorder_dep_path, 'a') as f:
print(name, file=f)
def _read_typeorder_dep(self):
try:
with open(self.typeorder_dep_path, 'r') as f:
return f.readlines()
except FileNotFoundError:
return []
def record_requirements(self):
"""Record requirements."""
order_dep_on = self._order_dep_on()
# Inject the predecessor, but not if its an override
# (this would leed to an circular dependency)
if (order_dep_on and 'CDIST_OVERRIDE' not in self.env):
try:
# __object_name is the name of the object whose type
# manifest is currently executed
__object_name = self.env.get('__object_name', None)
# load object name created befor this one from typeorder
# dep file
if __object_name:
parent = self.cdist_object.object_from_name(
__object_name)
typeorder = parent.typeorder_dep
else:
typeorder = self._read_typeorder_dep()
# get the type created before this one
lastcreatedtype = typeorder[-2].strip()
if 'require' in self.env:
if lastcreatedtype not in self.env['require']:
self.env['require'] += " " + lastcreatedtype
else:
self.env['require'] = lastcreatedtype
self.log.debug(("Injecting require for "
"CDIST_ORDER_DEPENDENCY: %s for %s"),
lastcreatedtype,
self.cdist_object.name)
except IndexError:
# if no second last line, we are on the first type,
# so do not set a requirement
pass
reqs = set()
if "require" in self.env:
requirements = self.env['require']
self.log.debug("reqs = " + requirements)
for requirement in requirements.split(" "):
# Ignore empty fields - probably the only field anyway
if len(requirement) == 0:
continue
object_name = self.record_requirement(requirement)
reqs.add(object_name)
if self._existing_reqs is not None:
# If object exists then compare existing and new requirements.
if self._existing_reqs != reqs:
warnmsg = ("Object {} already exists with requirements:\n"
"{}: {}\n"
"{}: {}\n"
"Dependency resolver could not handle dependencies "
"as expected.".format(
self.cdist_object.name,
" ".join(self.cdist_object.source),
self._existing_reqs,
self.object_source,
reqs
))
self.log.warning(warnmsg)
def record_auto_requirements(self):
"""An object shall automatically depend on all objects that it
defined in it's type manifest.
"""
# __object_name is the name of the object whose type manifest is
# currently executed
__object_name = self.env.get('__object_name', None)
if __object_name:
# The object whose type manifest is currently run
parent = self.cdist_object.object_from_name(__object_name)
# The object currently being defined
current_object = self.cdist_object
# As parent defined current_object it shall automatically
# depend on it.
# But only if the user hasn't said otherwise.
# Must prevent circular dependencies.
if parent.name not in current_object.requirements:
self.log.debug("Recording autorequirement %s for %s",
current_object.name, parent.name)
parent.autorequire.append(current_object.name)