# -*- coding: utf-8 -*- # # 2011-2013 Nico Schottelius (nico-cdist at schottelius.org) # 2012 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 . # # import argparse import logging import os import sys import cdist from cdist import core 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 """ 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'] # Internally only self.object_source = self.env['__cdist_manifest'] self.type_base_path = self.env['__cdist_type_base_path'] 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.type_name = os.path.basename(argv[0]) self.cdist_type = core.CdistType(self.type_base_path, self.type_name) self.__init_log() def run(self): """Emulate type commands (i.e. __file and co)""" self.commandline() self.setup_object() self.save_stdin() self.record_requirements() self.record_auto_requirements() self.log.debug("Finished %s %s" % (self.cdist_object.path, self.parameters)) def __init_log(self): """Setup logging facility""" if '__cdist_debug' in self.env: logging.root.setLevel(logging.DEBUG) else: logging.root.setLevel(logging.INFO) self.log = logging.getLogger(self.target_host) 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 parser.add_argument(argument, dest=parameter, action='store', required=False, default=self.cdist_type.parameter_defaults.get(parameter, None)) for parameter in self.cdist_type.optional_multiple_parameters: argument = "--" + parameter parser.add_argument(argument, dest=parameter, action='append', required=False, default=DefaultList.create(self.cdist_type.parameter_defaults.get(parameter, None))) 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.debug('Args: %s' % self.args) def setup_object(self): # Setup object_id - FIXME: unset / do not setup anymore! if not self.cdist_type.is_singleton: 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_id) # 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 not 'CDIST_OVERRIDE' in os.environ: if self.cdist_object.parameters != self.parameters: raise cdist.Error("Object %s already exists with conflicting parameters:\n%s: %s\n%s: %s" % (self.cdist_object.name, " ".join(self.cdist_object.source), self.cdist_object.parameters, self.object_source, self.parameters) ) 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 / 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_requirements(self): """record requirements""" if "CDIST_ORDER_DEPENDENCY" in self.env: # load object name created bevor this one from typeorder file ... with open(self.typeorder_path, 'r') as typecreationfile: typecreationorder = typecreationfile.readlines() # get the type created bevore this one ... try: lastcreatedtype = typecreationorder[-2].strip() if 'require' in self.env: 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 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 # Raises an error, if object cannot be created try: cdist_object = self.cdist_object.object_from_name(requirement) except core.cdist_type.NoSuchTypeError 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 as e: 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", requirement) # 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) 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 not parent.name in current_object.requirements: parent.autorequire.append(current_object.name)