diff --git a/.gitignore b/.gitignore index d606aec7..69a8ea98 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,8 @@ doc/man/man7/cdist-type__*.text doc/man/man7/cdist-reference.text doc/man/man*/docbook-xsl.css -# Ignore cache for version control -cache/ +# Ignore cdist cache for version control +/cache/ -# Python -bin/__pycache__/ +# Python / cache +__pycache__/ diff --git a/README b/README index 5e60a093..daa12f70 100644 --- a/README +++ b/README @@ -78,6 +78,7 @@ cdist was tested or is know to run on at least * A posix like shell * Python (>= 3.2 required) * SSH-Client + * Asciidoc (for building the manpages) ### Client ("target host") @@ -98,7 +99,7 @@ To install cdist, execute the following commands: cd cdist export PATH=$PATH:$(pwd -P)/bin - # If you want the manpages (requires gmake and asciidoc to be installed) + # If you want the manpages ./build.sh man export MANPATH=$MANPATH:$(pwd -P)/doc/man diff --git a/bin/cdist b/bin/cdist index 5ce947ef..5e1b96bf 100755 --- a/bin/cdist +++ b/bin/cdist @@ -32,753 +32,94 @@ import stat import sys import tempfile -BANNER = """ - .. . .x+=:. s - dF @88> z` ^% :8 - '88bu. %8P . is an object - if content == DOT_CDIST: - object_paths.append(starting_point) - - return object_paths - - def get_type_from_object(self, cdist_object): - """Returns the first part (i.e. type) of an object""" - return cdist_object.split(os.sep)[0] - - def get_object_id_from_object(self, cdist_object): - """Returns everything but the first part (i.e. object_id) of an object""" - return os.sep.join(cdist_object.split(os.sep)[1:]) - - def object_dir(self, cdist_object): - """Returns the full path to the object (including .cdist)""" - return os.path.join(self.object_base_dir, cdist_object, DOT_CDIST) - - def remote_object_dir(self, cdist_object): - """Returns the remote full path to the object (including .cdist)""" - return os.path.join(REMOTE_OBJECT_DIR, cdist_object, DOT_CDIST) - - def object_parameter_dir(self, cdist_object): - """Returns the dir to the object parameter""" - return os.path.join(self.object_dir(cdist_object), "parameter") - - def remote_object_parameter_dir(self, cdist_object): - """Returns the remote dir to the object parameter""" - return os.path.join(self.remote_object_dir(cdist_object), "parameter") - - def object_code_paths(self, cdist_object): - """Return paths to code scripts of object""" - return [os.path.join(self.object_dir(cdist_object), "code-local"), - os.path.join(self.object_dir(cdist_object), "code-remote")] - - def list_objects(self): - """Return list of existing objects""" - - objects = [] - if os.path.isdir(self.object_base_dir): - object_paths = self.list_object_paths(self.object_base_dir) - - for path in object_paths: - objects.append(os.path.relpath(path, self.object_base_dir)) - - return objects - - def type_dir(self, type, *args): - """Return directory the type""" - return os.path.join(self.type_base_dir, type, *args) - - def remote_type_explorer_dir(self, type): - """Return remote directory that holds the explorers of a type""" - return os.path.join(REMOTE_TYPE_DIR, type, "explorer") - - def transfer_object_parameter(self, cdist_object): - """Transfer the object parameter to the remote destination""" - # Create base path before using mkdir -p - self.remote_mkdir(self.remote_object_parameter_dir(cdist_object)) - - # Synchronise parameter dir afterwards - self.transfer_dir(self.object_parameter_dir(cdist_object), - self.remote_object_parameter_dir(cdist_object)) - - def transfer_global_explorers(self): - """Transfer the global explorers""" - self.remote_mkdir(REMOTE_GLOBAL_EXPLORER_DIR) - self.transfer_dir(self.global_explorer_dir, REMOTE_GLOBAL_EXPLORER_DIR) - - def transfer_type_explorers(self, type): - """Transfer explorers of a type, but only once""" - if type in self.type_explorers_transferred: - log.debug("Skipping retransfer for explorers of %s", type) - return - else: - # Do not retransfer - self.type_explorers_transferred[type] = 1 - - src = self.type_dir(type, "explorer") - remote_base = os.path.join(REMOTE_TYPE_DIR, type) - dst = self.remote_type_explorer_dir(type) - - # Only continue, if there is at least the directory - if os.path.isdir(src): - # Ensure that the path exists - self.remote_mkdir(remote_base) - self.transfer_dir(src, dst) - - - def link_type_to_emulator(self): - """Link type names to cdist-type-emulator""" - source = os.path.abspath(sys.argv[0]) - for type in self.list_types(): - destination = os.path.join(self.bin_dir, type) - log.debug("Linking %s to %s", source, destination) - os.symlink(source, destination) - - def run_global_explores(self): - """Run global explorers""" - explorers = self.list_global_explorers() - if(len(explorers) == 0): - exit_error("No explorers found in", self.global_explorer_dir) - - self.transfer_global_explorers() - for explorer in explorers: - output = self.global_explorer_output_path(explorer) - output_fd = open(output, mode='w') - cmd = [] - cmd.append("__explorer=" + REMOTE_GLOBAL_EXPLORER_DIR) - cmd.append(self.remote_global_explorer_path(explorer)) - - self.run_or_fail(cmd, stdout=output_fd, remote=True) - output_fd.close() - - def run_type_explorer(self, cdist_object): - """Run type specific explorers for objects""" - # Based on bin/cdist-object-explorer-run - - # Transfering explorers for this type - type = self.get_type_from_object(cdist_object) - self.transfer_type_explorers(type) - - cmd = [] - cmd.append("__explorer=" + REMOTE_GLOBAL_EXPLORER_DIR) - cmd.append("__type_explorer=" + self.remote_type_explorer_dir(type)) - cmd.append("__object=" + self.remote_object_dir(cdist_object)) - cmd.append("__object_id=" + self.get_object_id_from_object(cdist_object)) - cmd.append("__object_fq=" + cdist_object) - - # Need to transfer at least the parameters for objects to be useful - self.transfer_object_parameter(cdist_object) - - explorers = self.list_type_explorers(type) - for explorer in explorers: - remote_cmd = cmd + [os.path.join(self.remote_type_explorer_dir(type), explorer)] - output = os.path.join(self.type_explorer_output_dir(cdist_object), explorer) - output_fd = open(output, mode='w') - log.debug("%s exploring %s using %s storing to %s", - cdist_object, explorer, remote_cmd, output) - - self.run_or_fail(remote_cmd, stdout=output_fd, remote=True) - output_fd.close() - - def init_deploy(self): - """Ensure the base directories are cleaned up""" - log.debug("Creating clean directory structure") - - self.remove_remote_dir(REMOTE_BASE_DIR) - self.remote_mkdir(REMOTE_BASE_DIR) - - def run_initial_manifest(self): - """Run the initial manifest""" - env = { "__manifest" : self.manifest_dir } - self.run_manifest(self.initial_manifest, extra_env=env) - - def run_type_manifest(self, cdist_object): - """Run manifest for a specific object""" - type = self.get_type_from_object(cdist_object) - manifest = self.type_dir(type, "manifest") - - log.debug("%s: Running %s", cdist_object, manifest) - if os.path.exists(manifest): - env = { "__object" : self.object_dir(cdist_object), - "__object_id": self.get_object_id_from_object(cdist_object), - "__object_fq": cdist_object, - "__type": self.type_dir(type) - } - self.run_manifest(manifest, extra_env=env) - - def run_manifest(self, manifest, extra_env=None): - """Run a manifest""" - log.debug("Running manifest %s, env=%s", manifest, extra_env) - env = os.environ.copy() - env['PATH'] = self.bin_dir + ":" + env['PATH'] - - # Information required in every manifest - env['__target_host'] = self.target_host - env['__global'] = self.out_dir - - # Legacy stuff to make cdist-type-emulator work - env['__cdist_core_dir'] = os.path.join(self.base_dir, "core") - env['__cdist_local_base_dir'] = self.temp_dir - - # Submit information to new type emulator - env['__cdist_manifest'] = manifest - env['__cdist_type_base_dir'] = self.type_base_dir - - # Other environment stuff - if extra_env: - env.update(extra_env) - - self.shell_run_or_debug_fail(manifest, [manifest], env=env) - - def object_run(self, cdist_object, mode): - """Run gencode or code for an object""" - log.debug("Running %s from %s", mode, cdist_object) - file=os.path.join(self.object_dir(cdist_object), "require") - requirements = file_to_list(file) - type = self.get_type_from_object(cdist_object) - - for requirement in requirements: - log.debug("Object %s requires %s", cdist_object, requirement) - self.object_run(requirement, mode=mode) - - # - # Setup env Variable: - # - env = os.environ.copy() - env['__target_host'] = self.target_host - env['__global'] = self.out_dir - env["__object"] = self.object_dir(cdist_object) - env["__object_id"] = self.get_object_id_from_object(cdist_object) - env["__object_fq"] = cdist_object - env["__type"] = self.type_dir(type) - - if mode == "gencode": - paths = [ - self.type_dir(type, "gencode-local"), - self.type_dir(type, "gencode-remote") - ] - for bin in paths: - if os.path.isfile(bin): - # omit "gen" from gencode and - outfile=os.path.join(self.object_dir(cdist_object), - os.path.basename(bin)[3:]) - - outfile_fd = open(outfile, "w") - - # Need to flush to ensure our write is done before stdout write - outfile_fd.write(CODE_HEADER) - outfile_fd.flush() - - self.shell_run_or_debug_fail(bin, [bin], env=env, stdout=outfile_fd) - outfile_fd.close() - - status = os.stat(outfile) - - # Remove output if empty, else make it executable - if status.st_size == len(CODE_HEADER): - os.unlink(outfile) - else: - # Add header and make executable - identically to 0o700 - os.chmod(outfile, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR) - - if mode == "code": - local_dir = self.object_dir(cdist_object) - remote_dir = self.remote_object_dir(cdist_object) - - bin = os.path.join(local_dir, "code-local") - if os.path.isfile(bin): - self.run_or_fail([bin], remote=False) - - - local_remote_code = os.path.join(local_dir, "code-remote") - remote_remote_code = os.path.join(remote_dir, "code-remote") - if os.path.isfile(local_remote_code): - self.transfer_file(local_remote_code, remote_remote_code) - self.run_or_fail([remote_remote_code], remote=True) - - def stage_prepare(self): - """Do everything for a deploy, minus the actual code stage""" - self.init_deploy() - self.run_global_explores() - self.run_initial_manifest() - - old_objects = [] - objects = self.list_objects() - - # Continue process until no new objects are created anymore - while old_objects != objects: - log.debug("Prepare stage") - old_objects = list(objects) - for cdist_object in objects: - if cdist_object in self.objects_prepared: - log.debug("Skipping rerun of object %s", cdist_object) - continue - else: - self.run_type_explorer(cdist_object) - self.run_type_manifest(cdist_object) - self.objects_prepared.append(cdist_object) - - objects = self.list_objects() - - def stage_run(self): - """The final (and real) step of deployment""" - log.debug("Actual run objects") - # Now do the final steps over the existing objects - for cdist_object in self.list_objects(): - log.debug("Run object: %s", cdist_object) - self.object_run(cdist_object, mode="gencode") - self.object_run(cdist_object, mode="code") - - def deploy_to(self): - """Mimic the old deploy to: Deploy to one host""" - log.info("Deploying to " + self.target_host) - time_start = datetime.datetime.now() - - self.stage_prepare() - self.stage_run() - - time_end = datetime.datetime.now() - duration = time_end - time_start - log.info("Finished run of %s in %s seconds", - self.target_host, - duration.total_seconds()) - - def deploy_and_cleanup(self): - """Do what is most often done: deploy & cleanup""" - self.deploy_to() - self.cleanup() - -def banner(args): - """Guess what :-)""" - print(BANNER) - sys.exit(0) - -def config(args): - """Configure remote system""" - process = {} - - time_start = datetime.datetime.now() - - for host in args.host: - c = Cdist(host, initial_manifest=args.manifest, home=args.cdist_home, debug=args.debug) - if args.parallel: - log.debug("Creating child process for %s", host) - process[host] = multiprocessing.Process(target=c.deploy_and_cleanup) - process[host].start() - else: - c.deploy_and_cleanup() - - if args.parallel: - for p in process.keys(): - log.debug("Joining %s", p) - process[p].join() - - time_end = datetime.datetime.now() - log.info("Total processing time for %s host(s): %s", len(args.host), - (time_end - time_start).total_seconds()) - -def install(args): - """Install remote system""" - process = {} - -def emulator(): - """Emulate type commands (i.e. __file and co)""" - type = os.path.basename(sys.argv[0]) - type_dir = os.path.join(os.environ['__cdist_type_base_dir'], type) - param_dir = os.path.join(type_dir, "parameter") - global_dir = os.environ['__global'] - object_source = os.environ['__cdist_manifest'] - - parser = argparse.ArgumentParser(add_help=False) - - # Setup optional parameters - for parameter in file_to_list(os.path.join(param_dir, "optional")): - argument = "--" + parameter - parser.add_argument(argument, action='store', required=False) - - # Setup required parameters - for parameter in file_to_list(os.path.join(param_dir, "required")): - argument = "--" + parameter - parser.add_argument(argument, action='store', required=True) - - # Setup positional parameter, if not singleton - - if not os.path.isfile(os.path.join(type_dir, "singleton")): - parser.add_argument("object_id", nargs=1) - - # And finally verify parameter - args = parser.parse_args(sys.argv[1:]) - - # Setup object_id - if os.path.isfile(os.path.join(type_dir, "singleton")): - object_id = "singleton" - else: - object_id = args.object_id[0] - del args.object_id - - # FIXME: / hardcoded - better portable solution available? - if object_id[0] == '/': - object_id = object_id[1:] - - # FIXME: verify object id - log.debug(args) - - object_dir = os.path.join(global_dir, "object", type, - object_id, DOT_CDIST) - param_out_dir = os.path.join(object_dir, "parameter") - - object_source_file = os.path.join(object_dir, "source") - - if os.path.exists(param_out_dir): - object_exists = True - old_object_source_fd = open(object_source_file, "r") - old_object_source = old_object_source_fd.readlines() - old_object_source_fd.close() - - else: - object_exists = False - try: - os.makedirs(param_out_dir, exist_ok=True) - except OSError as error: - exit_error(param_out_dir + ": " + error.args[1]) - - # Record parameter - params = vars(args) - for param in params: - value = getattr(args, param) - if value: - file = os.path.join(param_out_dir, param) - log.debug(file + "<-" + param + " = " + value) - - # Already exists, verify all parameter are the same - if object_exists: - if not os.path.isfile(file): - print("New parameter + " + param + "specified, aborting") - print("Source = " + old_object_source + "new =" + object_source) - sys.exit(1) - else: - param_fd = open(file, "r") - param_old = param_fd.realines() - param_fd.close() - - if(param_old != param): - print("Parameter differs: " + param_old + "vs," + param) - print("Source = " + old_object_source + "new =" + object_source) - sys.exit(1) - else: - param_fd = open(file, "w") - param_fd.writelines(value) - param_fd.close() - - # Record requirements - if "__require" in os.environ: - requirements = os.environ['__require'] - print(object_id + ":Writing requirements: " + requirements) - require_fd = open(os.path.join(object_dir, "require"), "a") - require_fd.writelines(requirements.split(" ")) - require_fd.close() - - # Record / Append source - source_fd = open(os.path.join(object_dir, "source"), "a") - source_fd.writelines(object_source) - source_fd.close() - - # sys.exit(1) - print("Finished " + type + "/" + object_id + repr(params)) +# Ensure our /lib/ is included into PYTHON_PATH +sys.path.insert(0, os.path.abspath( + os.path.join(os.path.dirname(os.path.realpath(__file__)), '../lib')) +) +TYPE_PREFIX = "__" def commandline(): - """Parse command line""" - # Construct parser others can reuse - parser = {} - # Options _all_ parsers have in common - parser['most'] = argparse.ArgumentParser(add_help=False) - parser['most'].add_argument('-d', '--debug', - help='Set log level to debug', action='store_true') + """Parse command line""" + # Construct parser others can reuse + parser = {} + # Options _all_ parsers have in common + parser['most'] = argparse.ArgumentParser(add_help=False) + parser['most'].add_argument('-d', '--debug', + help='Set log level to debug', action='store_true') - # Main subcommand parser - parser['main'] = argparse.ArgumentParser(description='cdist ' + VERSION) - parser['main'].add_argument('-V', '--version', - help='Show version', action='version', - version='%(prog)s ' + VERSION) - parser['sub'] = parser['main'].add_subparsers(title="Commands") + # Main subcommand parser + parser['main'] = argparse.ArgumentParser(description='cdist ' + cdist.VERSION) + parser['main'].add_argument('-V', '--version', + help='Show version', action='version', + version='%(prog)s ' + cdist.VERSION) + parser['sub'] = parser['main'].add_subparsers(title="Commands") - # Banner - parser['banner'] = parser['sub'].add_parser('banner', - add_help=False) - parser['banner'].set_defaults(func=banner) + # Banner + parser['banner'] = parser['sub'].add_parser('banner', + add_help=False) + parser['banner'].set_defaults(func=cdist.banner.banner) - # Config and install (common stuff) - parser['configinstall'] = argparse.ArgumentParser(add_help=False) - parser['configinstall'].add_argument('host', nargs='+', - help='one or more hosts to operate on') - parser['configinstall'].add_argument('-c', '--cdist-home', - help='Change cdist home (default: .. from bin directory)', - action='store') - parser['configinstall'].add_argument('-i', '--initial-manifest', - help='Path to a cdist manifest', - dest='manifest', required=False) - parser['configinstall'].add_argument('-p', '--parallel', - help='Operate on multiple hosts in parallel', - action='store_true', dest='parallel') - parser['configinstall'].add_argument('-s', '--sequential', - help='Operate on multiple hosts sequentially (default)', - action='store_false', dest='parallel') + # Config and install (common stuff) + parser['configinstall'] = argparse.ArgumentParser(add_help=False) + parser['configinstall'].add_argument('host', nargs='+', + help='one or more hosts to operate on') + parser['configinstall'].add_argument('-c', '--cdist-home', + help='Change cdist home (default: .. from bin directory)', + action='store') + parser['configinstall'].add_argument('-i', '--initial-manifest', + help='Path to a cdist manifest', + dest='manifest', required=False) + parser['configinstall'].add_argument('-p', '--parallel', + help='Operate on multiple hosts in parallel', + action='store_true', dest='parallel') + parser['configinstall'].add_argument('-s', '--sequential', + help='Operate on multiple hosts sequentially (default)', + action='store_false', dest='parallel') - # Config - parser['config'] = parser['sub'].add_parser('config', - parents=[parser['most'], parser['configinstall']]) - parser['config'].set_defaults(func=config) + # Config + parser['config'] = parser['sub'].add_parser('config', + parents=[parser['most'], parser['configinstall']]) + parser['config'].set_defaults(func=cdist.config.config) - # Install - parser['install'] = parser['sub'].add_parser('install', - parents=[parser['most'], parser['configinstall']]) - parser['install'].set_defaults(func=install) + # Install + parser['install'] = parser['sub'].add_parser('install', + parents=[parser['most'], parser['configinstall']]) + parser['install'].set_defaults(func=cdist.install.install) - for p in parser: - parser[p].epilog = "Get cdist at http://www.nico.schottelius.org/software/cdist/" + for p in parser: + parser[p].epilog = "Get cdist at http://www.nico.schottelius.org/software/cdist/" - args = parser['main'].parse_args(sys.argv[1:]) + args = parser['main'].parse_args(sys.argv[1:]) - # Most subcommands have --debug, so handle it here - if 'debug' in args: - if args.debug: - logging.root.setLevel(logging.DEBUG) - log.debug(args) + # Most subcommands have --debug, so handle it here + if 'debug' in args: + if args.debug: + logging.root.setLevel(logging.DEBUG) + log.debug(args) - args.func(args) + args.func(args) if __name__ == "__main__": - try: - if re.match(TYPE_PREFIX, os.path.basename(sys.argv[0])): - emulator() - else: - commandline() - except KeyboardInterrupt: - sys.exit(0) + try: + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + if re.match(TYPE_PREFIX, os.path.basename(sys.argv[0])): + import cdist.emulator + cdist.emulator.emulator(sys.argv) + else: + import cdist + import cdist.banner + import cdist.config + import cdist.exec + import cdist.install + import cdist.path + + commandline() + except KeyboardInterrupt: + sys.exit(0) + except cdist.Error as e: + log.error(e) + sys.exit(1) diff --git a/bin/cdist.py b/bin/cdist.py deleted file mode 120000 index 9a039b33..00000000 --- a/bin/cdist.py +++ /dev/null @@ -1 +0,0 @@ -cdist \ No newline at end of file diff --git a/conf/explorer/os b/conf/explorer/os index e922c067..3e1582ec 100755 --- a/conf/explorer/os +++ b/conf/explorer/os @@ -65,8 +65,13 @@ if [ -f /etc/SuSE-release ]; then exit 0 fi +if uname -r | grep -s '.owl' >/dev/null 2>&1; then + echo owl + exit 0 +fi + if [ -f /etc/cdist-preos ]; then - echo preos + echo cdist-preos exit 0 fi diff --git a/conf/type/__partition_msdos/man.text b/conf/type/__partition_msdos/man.text new file mode 100644 index 00000000..c9ef0cf1 --- /dev/null +++ b/conf/type/__partition_msdos/man.text @@ -0,0 +1,64 @@ +cdist-type__partition_msdos(7) +============================== +Steven Armstrong + + +NAME +---- +cdist-type__partition_msdos - creates msdos partitions + + +DESCRIPTION +----------- +This cdist type allows you to create msdos paritions. + + +REQUIRED PARAMETERS +------------------- +type:: + the partition type used in fdisk (such as 82 or 83) or "extended" + + +OPTIONAL PARAMETERS +------------------- +partition:: + defaults to object_id +bootable:: + mark partition as bootable, true or false, defaults to false +size:: + the size of the partition (such as 32M or 15G, whole numbers + only), '+' for remaining space, or 'n%' for percentage of remaining + (these should only be used after all specific partition sizes are + specified). Defaults to +. + + +EXAMPLES +-------- + +-------------------------------------------------------------------------------- +# 128MB linux, bootable +__partition_msdos /dev/sda1 --type 83 --size 128M --bootable true +# 512MB swap +__partition_msdos /dev/sda2 --type 82 --size 512M +# extended +__partition_msdos /dev/sda3 --type extended --size 100G +# 10GB, linux +__partition_msdos /dev/sda5 --type 83 --size 10G +# 50% of free space, linux +__partition_msdos /dev/sda6 --type 83 --size 50% +# rest of disk, linux +__partition_msdos /dev/sda7 --type 83 --size + +# same thing as +__partition_msdos /dev/sda7 --type 83 +-------------------------------------------------------------------------------- + + +SEE ALSO +-------- +- cdist-type(7) + + +COPYING +------- +Copyright \(C) 2011 Steven Armstrong. Free use of this software is +granted under the terms of the GNU General Public License version 3 (GPLv3). diff --git a/conf/type/__partition_msdos/manifest b/conf/type/__partition_msdos/manifest new file mode 100755 index 00000000..0d73c405 --- /dev/null +++ b/conf/type/__partition_msdos/manifest @@ -0,0 +1,41 @@ +#!/bin/sh +# +# 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# +# 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 . +# + +# set defaults +if [ -f "$__object/parameter/partition" ]; then + partition="(cat "$__object/parameter/partition")" +else + partition="/$__object_id" + echo "$partition" > "$__object/parameter/partition" +fi +device="$(echo "$partition" | sed 's/[0-9]//g')" +echo "$device" > "$__object/parameter/device" +minor="$(echo "$partition" | sed 's/[^0-9]//g')" +echo "$minor" > "$__object/parameter/minor" + +if [ ! -f "$__object/parameter/bootable" ]; then + echo "false" > "$__object/parameter/bootable" +fi +if [ ! -f "$__object/parameter/size" ]; then + echo "+" > "$__object/parameter/size" +fi + +# pull in the type that actually does something with the above parameters +require="$__self" __partition_msdos_apply diff --git a/conf/type/__partition_msdos/parameter/optional b/conf/type/__partition_msdos/parameter/optional new file mode 100644 index 00000000..b2b0a4c2 --- /dev/null +++ b/conf/type/__partition_msdos/parameter/optional @@ -0,0 +1,3 @@ +partition +bootable +size diff --git a/conf/type/__partition_msdos/parameter/required b/conf/type/__partition_msdos/parameter/required new file mode 100644 index 00000000..aa80e646 --- /dev/null +++ b/conf/type/__partition_msdos/parameter/required @@ -0,0 +1 @@ +type diff --git a/conf/type/__partition_msdos_apply/explorer/partitions b/conf/type/__partition_msdos_apply/explorer/partitions new file mode 100755 index 00000000..6be61af4 --- /dev/null +++ b/conf/type/__partition_msdos_apply/explorer/partitions @@ -0,0 +1,3 @@ +#!/bin/sh + +cat /proc/partitions diff --git a/conf/type/__partition_msdos_apply/files/lib.sh b/conf/type/__partition_msdos_apply/files/lib.sh new file mode 100644 index 00000000..0e9705d9 --- /dev/null +++ b/conf/type/__partition_msdos_apply/files/lib.sh @@ -0,0 +1,58 @@ +die() { + echo "[__partition_msdos_apply] $@" >&2 + exit 1 +} +debug() { + echo "[__partition_msdos_apply] $@" >&2 +} + +fdisk_command() { + local device=$1 + local cmd=$2 + + debug fdisk_command "running fdisk command '${cmd}' on device ${device}" + echo -en "${cmd}\nw\n" | fdisk -c -u "$device" + return $? +} + +create_disklabel() { + local device=$1 + + debug create_disklabel "creating new msdos disklabel" + fdisk_command ${device} "o" + return $? +} + +create_partition() { + local device=$1 + local minor=$2 + local size=$3 + local type=$4 + local primary_count=$5 + + if [ "$type" = "extended" -o "$type" = "5" ]; then + # Extended partition + primary_extended="e\n" + first_minor="${minor}\n" + [ "${minor}" = "4" ] && first_minor="" + type_minor="${minor}\n" + [ "${minor}" = "1" ] && type_minor="" + type="5" + elif [ "${minor}" -lt "5" ]; then + primary_extended="p\n" + first_minor="${minor}\n" + [ "${minor}" = "4" ] && first_minor="" + type_minor="${minor}\n" + [ "${minor}" = "1" ] && type_minor="" + else + # Logical partitions + first_minor="${minor}\n" + type_minor="${minor}\n" + primary_extended="l\n" + [ "$primary_count" > "3" ] && primary_extended="" + fi + [ -n "${size}" ] && size="+${size}M" + fdisk_command ${device} "n\n${primary_extended}${first_minor}\n${size}\nt\n${type_minor}${type}\n" + return $? +} + diff --git a/conf/type/__partition_msdos_apply/gencode-remote b/conf/type/__partition_msdos_apply/gencode-remote new file mode 100755 index 00000000..d352abdb --- /dev/null +++ b/conf/type/__partition_msdos_apply/gencode-remote @@ -0,0 +1,110 @@ +#!/bin/sh +# +# 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# +# 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 . +# + +die() { + echo "[__partition_msdos_apply] $@" >&2 + exit 1 +} +debug() { + echo "[__partition_msdos_apply] $@" >&2 +} + +# Convert a size specifier 1G 100M or 50% into the corresponding numeric MB. +size_to_mb() { + local size=$1 + local available_size="$2" + + local number_suffix="$(echo ${size} | sed -e 's:\.[0-9]\+::' -e 's:\([0-9]\+\)\([MmGg%]\)[Bb]\?:\1|\2:')" + local number="$(echo ${number_suffix} | cut -d '|' -f1)" + local suffix="$(echo ${number_suffix} | cut -d '|' -f2)" + + case "$suffix" in + M|m) + size="$number" + ;; + G|g) + size="$(( $number * 1024 ))" + ;; + %) + size="$(( $available_size * $number / 100 ))" + ;; + *) + size="-1" + esac + echo "$size" +} + +# include function library for use on target +cat "$__type/files/lib.sh" + +partitions="$__object/explorer/partitions" +objects=$(find "$__global/object/__partition_msdos" -path "*.cdist") +current_device="" +available_size= +primary_count=0 +for object in $objects; do + device="$(cat "$object/parameter/device")" + if [ "$current_device" != "$device" ]; then + echo "create_disklabel $device" + current_device="$device" + device_name=$(echo ${device} | sed -e 's:^/dev/::;s:/:\\/:g') + available_size=$(( $(awk "/${device_name}\$/ { print \$3; }" "$partitions") / 1024)) + # make sure we don't go past the end of the drive + available_size=$((available_size - 2)) + primary_count=0 + debug "----- $device" + debug "current_device=$current_device" + debug "available_size=$available_size" + fi + + type="$(cat "$object/parameter/type")" + partition="$(cat "$object/parameter/partition")" + minor="$(cat "$object/parameter/minor")" + + if [ "${minor}" -lt "5" ]; then + primary_count=$(( $primary_count + 1 )) + fi + bootable="$(cat "$object/parameter/bootable")" + size="$(cat "$object/parameter/size")" + if [ "$size" = "+" ]; then + # use rest of device + partition_size="" + available_size=0 + else + partition_size=$(size_to_mb "$size" "$available_size") + available_size="$(( $available_size - $partition_size ))" + fi + + [ "$partition_size" = "-1" ] && die "could not translate size '$size' to a usable value" + debug "----- $partition" + debug "primary_count=$primary_count" + debug "current_device=$current_device" + debug "device=$device" + debug "type=$type" + debug "partition=$partition" + debug "minor=$minor" + debug "bootable=$bootable" + debug "size=$size" + debug "partition_size=$partition_size" + debug "available_size=$available_size" + + echo "create_partition $device $minor $partition_size $type $primary_count" +done + diff --git a/conf/type/__partition_msdos_apply/man.text b/conf/type/__partition_msdos_apply/man.text new file mode 100644 index 00000000..4d4f127c --- /dev/null +++ b/conf/type/__partition_msdos_apply/man.text @@ -0,0 +1,42 @@ +cdist-type__partition_msdos_apply(7) +==================================== +Steven Armstrong + + +NAME +---- +cdist-type__partition_msdos_apply + + +DESCRIPTION +----------- +Create the partitions defined with __partition_msdos + + +REQUIRED PARAMETERS +------------------- +None + + +OPTIONAL PARAMETERS +------------------- +None. + + +EXAMPLES +-------- + +-------------------------------------------------------------------------------- +__partition_msdos_apply +-------------------------------------------------------------------------------- + + +SEE ALSO +-------- +- cdist-type(7) +- cdist-type__partition_msdos_apply(7) + +COPYING +------- +Copyright \(C) 2011 Steven Armstrong. Free use of this software is +granted under the terms of the GNU General Public License version 3 (GPLv3). diff --git a/conf/type/__partition_msdos_apply/singleton b/conf/type/__partition_msdos_apply/singleton new file mode 100644 index 00000000..e69de29b diff --git a/doc/changelog b/doc/changelog index cc6aa78b..a08efb34 100644 --- a/doc/changelog +++ b/doc/changelog @@ -1,5 +1,10 @@ -2.0.1: - * Bugfix cdist: Always print source of error in case of exec errors +2.0.2: + * Add support for detection of OpenWall Linux (Matthias Teege) + +2.0.1: 2011-09-23 + * Bugfix core: Always print source of error in case of exec errors + * Bugfix core: Various smaller bugs in string concatenation + * Feature: Add marker "changed" to changed objects 2.0.0: 2011-09-16 * New Type: __package_rubygem (Chase Allen James) diff --git a/doc/dev/todo/TAKEME b/doc/dev/todo/TAKEME index 5439a1b9..8be1da54 100644 --- a/doc/dev/todo/TAKEME +++ b/doc/dev/todo/TAKEME @@ -9,6 +9,12 @@ CORE - allow cdist to run without $PATH setup: ./bin/cdist-deploy-to - support non-ssh access? +TESTS +----- +- multiple defines of object: + - fail if different parameters + - succeed if same parameters + USER INTERFACE -------------- - add support $__tmp? diff --git a/doc/dev/todo/niconext b/doc/dev/todo/niconext index 84745512..4e07dd96 100644 --- a/doc/dev/todo/niconext +++ b/doc/dev/todo/niconext @@ -1,18 +1,13 @@ -2.0.1: - -- Rewrite cdist-type-emulator - - Remove legacy code in cdist - - Remove cdist-config - - Remove man1/cdist-type-emulator.text - - Remove the PATH=... part from the README - - - how to access output dir? - - Test: - __cdist_type_base_dir=$(pwd -P)/conf/type __file - - Fix / rewrite cdist-quickstart +- write tutorial!!!!!!!!! + - like ccollect! + - include ssh control master! + - add local/ hint (and add to git) + - add hint for ssh StrictHostKeyChecking no + - and that ssh will wait for answer of prompt + - nasty if used in parallel mode (scroll up!) + -------------------------------------------------------------------------------- - Initial install support @@ -41,3 +36,7 @@ http://www.youtube.com/watch?v=PRMjzy48eTI - Setup __debug, if -d is given, so other tools can reuse it + (-> non core feature! + +- remote_prefix: + scp vs. ssh issue diff --git a/doc/man/cdist-reference.text.sh b/doc/man/cdist-reference.text.sh index e38f157d..c205bdcc 100755 --- a/doc/man/cdist-reference.text.sh +++ b/doc/man/cdist-reference.text.sh @@ -154,6 +154,16 @@ done cat << eof +OBJECTS +------- +For object to object communication and tests, the following paths are +usable within a object directory: + +changed:: + This empty file exists in an object directory, if the object has + code to be excuted (either remote or local) + + ENVIRONMENT VARIABLES --------------------- __explorer:: diff --git a/lib/cdist/__init__.py b/lib/cdist/__init__.py new file mode 100644 index 00000000..192e5001 --- /dev/null +++ b/lib/cdist/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# 2010-2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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 . +# +# + +VERSION = "2.0.2" + +class Error(Exception): + """Base exception class for this project""" + pass diff --git a/lib/cdist/banner.py b/lib/cdist/banner.py new file mode 100644 index 00000000..a07deea9 --- /dev/null +++ b/lib/cdist/banner.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# 2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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 logging +import sys + +log = logging.getLogger(__name__) + +BANNER = """ + .. . .x+=:. s + dF @88> z` ^% :8 + '88bu. %8P . . +# +# + +import datetime +import logging +import os +import stat + +log = logging.getLogger(__name__) + +import cdist.path + +CODE_HEADER = "#!/bin/sh -e\n" + +class Config: + """Cdist main class to hold arbitrary data""" + + def __init__(self, target_host, + initial_manifest=False, remote_user="root", + home=None, debug=False): + + self.target_host = target_host + self.debug = debug + self.remote_user = remote_user + self.remote_prefix = ["ssh", self.remote_user + "@" + self.target_host] + + self.path = cdist.path.Path(self.target_host, + initial_manifest=initial_manifest, + remote_user=self.remote_user, + remote_prefix=self.remote_prefix, + base_dir=home, + debug=debug) + + self.objects_prepared = [] + + def cleanup(self): + self.path.cleanup() + + def run_global_explores(self): + """Run global explorers""" + explorers = self.path.list_global_explorers() + if(len(explorers) == 0): + raise CdistError("No explorers found in", self.path.global_explorer_dir) + + self.path.transfer_global_explorers() + for explorer in explorers: + output = self.path.global_explorer_output_path(explorer) + output_fd = open(output, mode='w') + cmd = [] + cmd.append("__explorer=" + cdist.path.REMOTE_GLOBAL_EXPLORER_DIR) + cmd.append(self.path.remote_global_explorer_path(explorer)) + + cdist.exec.run_or_fail(cmd, stdout=output_fd, remote_prefix=self.remote_prefix) + output_fd.close() + + def run_type_explorer(self, cdist_object): + """Run type specific explorers for objects""" + + type = self.path.get_type_from_object(cdist_object) + self.path.transfer_type_explorers(type) + + cmd = [] + cmd.append("__explorer=" + cdist.path.REMOTE_GLOBAL_EXPLORER_DIR) + cmd.append("__type_explorer=" + self.path.remote_type_explorer_dir(type)) + cmd.append("__object=" + self.path.remote_object_dir(cdist_object)) + cmd.append("__object_id=" + self.path.get_object_id_from_object(cdist_object)) + cmd.append("__object_fq=" + cdist_object) + + # Need to transfer at least the parameters for objects to be useful + self.path.transfer_object_parameter(cdist_object) + + explorers = self.path.list_type_explorers(type) + for explorer in explorers: + remote_cmd = cmd + [os.path.join(self.path.remote_type_explorer_dir(type), explorer)] + output = os.path.join(self.path.type_explorer_output_dir(cdist_object), explorer) + output_fd = open(output, mode='w') + log.debug("%s exploring %s using %s storing to %s", + cdist_object, explorer, remote_cmd, output) + + cdist.exec.run_or_fail(remote_cmd, stdout=output_fd, remote_prefix=self.remote_prefix) + output_fd.close() + + def init_deploy(self): + """Ensure the base directories are cleaned up""" + log.debug("Creating clean directory structure") + + self.path.remove_remote_dir(cdist.path.REMOTE_BASE_DIR) + self.path.remote_mkdir(cdist.path.REMOTE_BASE_DIR) + + def run_initial_manifest(self): + """Run the initial manifest""" + env = { "__manifest" : self.path.manifest_dir } + self.run_manifest(self.path.initial_manifest, extra_env=env) + + def run_type_manifest(self, cdist_object): + """Run manifest for a specific object""" + type = self.path.get_type_from_object(cdist_object) + manifest = self.path.type_dir(type, "manifest") + + log.debug("%s: Running %s", cdist_object, manifest) + if os.path.exists(manifest): + env = { "__object" : self.path.object_dir(cdist_object), + "__object_id": self.path.get_object_id_from_object(cdist_object), + "__object_fq": cdist_object, + "__type": self.path.type_dir(type) + } + self.run_manifest(manifest, extra_env=env) + + def run_manifest(self, manifest, extra_env=None): + """Run a manifest""" + log.debug("Running manifest %s, env=%s", manifest, extra_env) + env = os.environ.copy() + env['PATH'] = self.path.bin_dir + ":" + env['PATH'] + + # Information required in every manifest + env['__target_host'] = self.target_host + env['__global'] = self.path.out_dir + + # Legacy stuff to make cdist-type-emulator work + env['__cdist_core_dir'] = os.path.join(self.path.base_dir, "core") + env['__cdist_local_base_dir'] = self.path.temp_dir + + # Submit information to new type emulator + + # Required for recording source + env['__cdist_manifest'] = manifest + + # Required to find types + env['__cdist_type_base_dir'] = self.path.type_base_dir + + # Other environment stuff + if extra_env: + env.update(extra_env) + + cdist.exec.shell_run_or_debug_fail(manifest, [manifest], env=env) + + def object_run(self, cdist_object, mode): + """Run gencode or code for an object""" + log.debug("Running %s from %s", mode, cdist_object) + file=os.path.join(self.path.object_dir(cdist_object), "require") + requirements = cdist.path.file_to_list(file) + type = self.path.get_type_from_object(cdist_object) + + for requirement in requirements: + log.debug("Object %s requires %s", cdist_object, requirement) + self.object_run(requirement, mode=mode) + + # + # Setup env Variable: + # + env = os.environ.copy() + env['__target_host'] = self.target_host + env['__global'] = self.path.out_dir + env["__object"] = self.path.object_dir(cdist_object) + env["__object_id"] = self.path.get_object_id_from_object(cdist_object) + env["__object_fq"] = cdist_object + env["__type"] = self.path.type_dir(type) + + if mode == "gencode": + paths = [ + self.path.type_dir(type, "gencode-local"), + self.path.type_dir(type, "gencode-remote") + ] + for bin in paths: + if os.path.isfile(bin): + # omit "gen" from gencode and use it for output base + outfile=os.path.join(self.path.object_dir(cdist_object), + os.path.basename(bin)[3:]) + + outfile_fd = open(outfile, "w") + + # Need to flush to ensure our write is done before stdout write + outfile_fd.write(CODE_HEADER) + outfile_fd.flush() + + cdist.exec.shell_run_or_debug_fail(bin, [bin], env=env, stdout=outfile_fd) + outfile_fd.close() + + status = os.stat(outfile) + + # Remove output if empty, else make it executable + if status.st_size == len(CODE_HEADER): + os.unlink(outfile) + else: + # Add header and make executable - identically to 0o700 + os.chmod(outfile, stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR) + + # Mark object as changed + open(os.path.join(self.path.object_dir(cdist_object), "changed"), "w").close() + + + if mode == "code": + local_dir = self.path.object_dir(cdist_object) + remote_dir = self.path.remote_object_dir(cdist_object) + + bin = os.path.join(local_dir, "code-local") + if os.path.isfile(bin): + cdist.exec.run_or_fail([bin]) + + + local_remote_code = os.path.join(local_dir, "code-remote") + remote_remote_code = os.path.join(remote_dir, "code-remote") + if os.path.isfile(local_remote_code): + self.path.transfer_file(local_remote_code, remote_remote_code) + # FIXME: remote_prefix + cdist.exec.run_or_fail([remote_remote_code], remote_prefix=self.remote_prefix) + + def stage_prepare(self): + """Do everything for a deploy, minus the actual code stage""" + self.init_deploy() + self.run_global_explores() + self.run_initial_manifest() + + old_objects = [] + objects = self.path.list_objects() + + # Continue process until no new objects are created anymore + while old_objects != objects: + log.debug("Prepare stage") + old_objects = list(objects) + for cdist_object in objects: + if cdist_object in self.objects_prepared: + log.debug("Skipping rerun of object %s", cdist_object) + continue + else: + self.run_type_explorer(cdist_object) + self.run_type_manifest(cdist_object) + self.objects_prepared.append(cdist_object) + + objects = self.path.list_objects() + + def stage_run(self): + """The final (and real) step of deployment""" + log.debug("Actual run objects") + # Now do the final steps over the existing objects + for cdist_object in self.path.list_objects(): + log.debug("Run object: %s", cdist_object) + self.object_run(cdist_object, mode="gencode") + self.object_run(cdist_object, mode="code") + + def deploy_to(self): + """Mimic the old deploy to: Deploy to one host""" + log.info("Deploying to " + self.target_host) + time_start = datetime.datetime.now() + + self.stage_prepare() + self.stage_run() + + time_end = datetime.datetime.now() + duration = time_end - time_start + log.info("Finished run of %s in %s seconds", + self.target_host, + duration.total_seconds()) + + def deploy_and_cleanup(self): + """Do what is most often done: deploy & cleanup""" + self.deploy_to() + self.cleanup() + +def config(args): + """Configure remote system""" + process = {} + + time_start = datetime.datetime.now() + + for host in args.host: + c = Config(host, initial_manifest=args.manifest, home=args.cdist_home, debug=args.debug) + if args.parallel: + log.debug("Creating child process for %s", host) + process[host] = multiprocessing.Process(target=c.deploy_and_cleanup) + process[host].start() + else: + c.deploy_and_cleanup() + + if args.parallel: + for p in process.keys(): + log.debug("Joining %s", p) + process[p].join() + + time_end = datetime.datetime.now() + log.info("Total processing time for %s host(s): %s", len(args.host), + (time_end - time_start).total_seconds()) + +def install(args): + """Install remote system""" + process = {} + +def commandline(): + """Parse command line""" + # Construct parser others can reuse + parser = {} + # Options _all_ parsers have in common + parser['most'] = argparse.ArgumentParser(add_help=False) + parser['most'].add_argument('-d', '--debug', + help='Set log level to debug', action='store_true') + + # Main subcommand parser + parser['main'] = argparse.ArgumentParser(description='cdist ' + cdist.VERSION) + parser['main'].add_argument('-V', '--version', + help='Show version', action='version', + version='%(prog)s ' + cdist.VERSION) + parser['sub'] = parser['main'].add_subparsers(title="Commands") + + # Banner + parser['banner'] = parser['sub'].add_parser('banner', + add_help=False) + parser['banner'].set_defaults(func=cdist.banner.banner) + + # Config and install (common stuff) + parser['configinstall'] = argparse.ArgumentParser(add_help=False) + parser['configinstall'].add_argument('host', nargs='+', + help='one or more hosts to operate on') + parser['configinstall'].add_argument('-c', '--cdist-home', + help='Change cdist home (default: .. from bin directory)', + action='store') + parser['configinstall'].add_argument('-i', '--initial-manifest', + help='Path to a cdist manifest', + dest='manifest', required=False) + parser['configinstall'].add_argument('-p', '--parallel', + help='Operate on multiple hosts in parallel', + action='store_true', dest='parallel') + parser['configinstall'].add_argument('-s', '--sequential', + help='Operate on multiple hosts sequentially (default)', + action='store_false', dest='parallel') + + # Config + parser['config'] = parser['sub'].add_parser('config', + parents=[parser['most'], parser['configinstall']]) + parser['config'].set_defaults(func=config) + + # Install + parser['install'] = parser['sub'].add_parser('install', + parents=[parser['most'], parser['configinstall']]) + parser['install'].set_defaults(func=install) + + for p in parser: + parser[p].epilog = "Get cdist at http://www.nico.schottelius.org/software/cdist/" + + args = parser['main'].parse_args(sys.argv[1:]) + + # Most subcommands have --debug, so handle it here + if 'debug' in args: + if args.debug: + logging.root.setLevel(logging.DEBUG) + log.debug(args) + + args.func(args) + + +if __name__ == "__main__": + try: + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + if re.match(TYPE_PREFIX, os.path.basename(sys.argv[0])): + cdist_lib = os.environ["__cdist_python_lib"] + sys.path.insert(0, cdist_lib) + import cdist.emulator + cdist.emulator.emulator(sys.argv) + else: + cdist_lib = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../lib')) + sys.path.insert(0, cdist_lib) + + import cdist + import cdist.banner + import cdist.exec + import cdist.path + + commandline() + except KeyboardInterrupt: + sys.exit(0) + except cdist.Error as e: + log.error(e) + sys.exit(1) diff --git a/lib/cdist/emulator.py b/lib/cdist/emulator.py new file mode 100755 index 00000000..ecdfff25 --- /dev/null +++ b/lib/cdist/emulator.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# 2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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.path + +log = logging.getLogger(__name__) + +def emulator(argv): + """Emulate type commands (i.e. __file and co)""" + type = os.path.basename(argv[0]) + type_dir = os.path.join(os.environ['__cdist_type_base_dir'], type) + param_dir = os.path.join(type_dir, "parameter") + global_dir = os.environ['__global'] + object_source = os.environ['__cdist_manifest'] + + parser = argparse.ArgumentParser(add_help=False) + + # Setup optional parameters + for parameter in cdist.path.file_to_list(os.path.join(param_dir, "optional")): + argument = "--" + parameter + parser.add_argument(argument, action='store', required=False) + + # Setup required parameters + for parameter in cdist.path.file_to_list(os.path.join(param_dir, "required")): + argument = "--" + parameter + parser.add_argument(argument, action='store', required=True) + + # Setup positional parameter, if not singleton + + if not os.path.isfile(os.path.join(type_dir, "singleton")): + parser.add_argument("object_id", nargs=1) + + # And finally verify parameter + args = parser.parse_args(argv[1:]) + + # Setup object_id + if os.path.isfile(os.path.join(type_dir, "singleton")): + object_id = "singleton" + else: + object_id = args.object_id[0] + del args.object_id + + # FIXME: / hardcoded - better portable solution available? + if object_id[0] == '/': + object_id = object_id[1:] + + # FIXME: verify object id + log.debug(args) + + object_dir = os.path.join(global_dir, "object", type, + object_id, cdist.path.DOT_CDIST) + param_out_dir = os.path.join(object_dir, "parameter") + + object_source_file = os.path.join(object_dir, "source") + + if os.path.exists(param_out_dir): + object_exists = True + old_object_source_fd = open(object_source_file, "r") + old_object_source = old_object_source_fd.readlines() + old_object_source_fd.close() + + else: + object_exists = False + try: + os.makedirs(param_out_dir, exist_ok=True) + except OSError as error: + raise CdistError(param_out_dir + ": " + error.args[1]) + + # Record parameter + params = vars(args) + for param in params: + value = getattr(args, param) + if value: + file = os.path.join(param_out_dir, param) + log.debug(file + "<-" + param + " = " + value) + + # Already exists, verify all parameter are the same + if object_exists: + if not os.path.isfile(file): + print("New parameter + " + param + "specified, aborting") + print("Source = " + old_object_source + "new =" + object_source) + sys.exit(1) + else: + param_fd = open(file, "r") + param_old = param_fd.readlines() + param_fd.close() + + if(param_old != param): + print("Parameter " + param + " differs: " + " ".join(param_old) + " vs. " + param) + print("Sources: " + " ".join(old_object_source) + " and " + object_source) + sys.exit(1) + else: + param_fd = open(file, "w") + param_fd.writelines(value) + param_fd.close() + + # Record requirements + if "__require" in os.environ: + requirements = os.environ['__require'] + print(object_id + ":Writing requirements: " + requirements) + require_fd = open(os.path.join(object_dir, "require"), "a") + require_fd.writelines(requirements.split(" ")) + require_fd.close() + + # Record / Append source + source_fd = open(os.path.join(object_dir, "source"), "a") + source_fd.writelines(object_source) + source_fd.close() + + # sys.exit(1) + print("Finished " + type + "/" + object_id + repr(params)) diff --git a/lib/cdist/exec.py b/lib/cdist/exec.py new file mode 100644 index 00000000..09e4e8a4 --- /dev/null +++ b/lib/cdist/exec.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# 2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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 logging +import subprocess + +log = logging.getLogger(__name__) + +def shell_run_or_debug_fail(script, *args, **kargs): + # Manually execute /bin/sh, because sh -e does what we want + # and sh -c -e does not exit if /bin/false called + args[0][:0] = [ "/bin/sh", "-e" ] + + remote = False + if "remote_prefix" in kargs: + remote = True + args[0][:0] = kargs["remote_prefix"] + del kargs["remote_prefix"] + + log.debug("Shell exec cmd: %s", args) + log.debug("Shell exec env: %s", kargs['env']) + try: + subprocess.check_call(*args, **kargs) + except subprocess.CalledProcessError: + log.error("Code that raised the error:\n") + if remote: + # FIXME: included in Path! + remote_cat(script) + else: + try: + script_fd = open(script) + print(script_fd.read()) + script_fd.close() + except IOError as error: + raise CdistError(str(error)) + + raise CdistError("Command failed (shell): " + " ".join(*args)) + except OSError as error: + raise CdistError(" ".join(*args) + ": " + error.args[1]) + + +def run_or_fail(*args, **kargs): + if "remote_prefix" in kargs: + args[0][:0] = kargs["remote_prefix"] + del kargs["remote_prefix"] + + log.debug("Exec: " + " ".join(*args)) + try: + subprocess.check_call(*args, **kargs) + except subprocess.CalledProcessError: + raise CdistError("Command failed: " + " ".join(*args)) + except OSError as error: + raise CdistError(" ".join(*args) + ": " + error.args[1]) diff --git a/lib/cdist/install.py b/lib/cdist/install.py new file mode 100755 index 00000000..98b388ec --- /dev/null +++ b/lib/cdist/install.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# 2010-2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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 logging + +log = logging.getLogger(__name__) + +def install(args): + """Install remote system""" + process = {} + diff --git a/lib/cdist/path.py b/lib/cdist/path.py new file mode 100644 index 00000000..0fa753a8 --- /dev/null +++ b/lib/cdist/path.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# +# 2010-2011 Nico Schottelius (nico-cdist at schottelius.org) +# +# 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 logging +import os +import shutil +import sys +import tempfile + +# Hardcoded paths usually not changable +REMOTE_BASE_DIR = "/var/lib/cdist" +REMOTE_CONF_DIR = os.path.join(REMOTE_BASE_DIR, "conf") +REMOTE_OBJECT_DIR = os.path.join(REMOTE_BASE_DIR, "object") +REMOTE_TYPE_DIR = os.path.join(REMOTE_CONF_DIR, "type") +REMOTE_GLOBAL_EXPLORER_DIR = os.path.join(REMOTE_CONF_DIR, "explorer") + +DOT_CDIST = ".cdist" + +log = logging.getLogger(__name__) + +import cdist.exec + +def file_to_list(filename): + """Return list from \n seperated file""" + if os.path.isfile(filename): + file_fd = open(filename, "r") + lines = file_fd.readlines() + file_fd.close() + + # Remove \n from all lines + lines = map(lambda s: s.strip(), lines) + else: + lines = [] + + return lines + +class Path: + """Class that handles path related configurations""" + + def __init__(self, + target_host, + remote_user, + remote_prefix, + initial_manifest=False, + base_dir=None, + debug=False): + + # Base and Temp Base + if base_dir: + self.base_dir = base_dir + else: + self.base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + + self.temp_dir = tempfile.mkdtemp() + self.target_host = target_host + + self.remote_user = remote_user + self.remote_prefix = remote_prefix + + self.conf_dir = os.path.join(self.base_dir, "conf") + self.cache_base_dir = os.path.join(self.base_dir, "cache") + self.cache_dir = os.path.join(self.cache_base_dir, target_host) + self.global_explorer_dir = os.path.join(self.conf_dir, "explorer") + self.lib_dir = os.path.join(self.base_dir, "lib") + self.manifest_dir = os.path.join(self.conf_dir, "manifest") + self.type_base_dir = os.path.join(self.conf_dir, "type") + + self.out_dir = os.path.join(self.temp_dir, "out") + os.mkdir(self.out_dir) + + self.global_explorer_out_dir = os.path.join(self.out_dir, "explorer") + os.mkdir(self.global_explorer_out_dir) + + self.object_base_dir = os.path.join(self.out_dir, "object") + + # Setup binary directory + contents + self.bin_dir = os.path.join(self.out_dir, "bin") + os.mkdir(self.bin_dir) + self.link_type_to_emulator() + + # List of type explorers transferred + self.type_explorers_transferred = {} + + # objects + self.objects_prepared = [] + + # Mostly static, but can be overwritten on user demand + if initial_manifest: + self.initial_manifest = initial_manifest + else: + self.initial_manifest = os.path.join(self.manifest_dir, "init") + + def cleanup(self): + # Do not use in __del__: + # http://docs.python.org/reference/datamodel.html#customization + # "other globals referenced by the __del__() method may already have been deleted + # or in the process of being torn down (e.g. the import machinery shutting down)" + # + log.debug("Saving" + self.temp_dir + "to " + self.cache_dir) + # Remove previous cache + if os.path.exists(self.cache_dir): + shutil.rmtree(self.cache_dir) + shutil.move(self.temp_dir, self.cache_dir) + + + def remote_mkdir(self, directory): + """Create directory on remote side""" + cdist.exec.run_or_fail(["mkdir", "-p", directory], remote_prefix=self.remote_prefix) + + def remote_cat(filename): + """Use cat on the remote side for output""" + cdist.exec.run_or_fail(["cat", filename], remote_prefix=self.remote_prefix) + + def remove_remote_dir(self, destination): + cdist.exec.run_or_fail(["rm", "-rf", destination], remote_prefix=self.remote_prefix) + + def transfer_dir(self, source, destination): + """Transfer directory and previously delete the remote destination""" + self.remove_remote_dir(destination) + cdist.exec.run_or_fail(["scp", "-qr", source, + self.remote_user + "@" + + self.target_host + ":" + + destination]) + + def transfer_file(self, source, destination): + """Transfer file""" + cdist.exec.run_or_fail(["scp", "-q", source, + self.remote_user + "@" + + self.target_host + ":" + + destination]) + + def global_explorer_output_path(self, explorer): + """Returns path of the output for a global explorer""" + return os.path.join(self.global_explorer_out_dir, explorer) + + def type_explorer_output_dir(self, cdist_object): + """Returns and creates dir of the output for a type explorer""" + dir = os.path.join(self.object_dir(cdist_object), "explorer") + if not os.path.isdir(dir): + os.mkdir(dir) + + return dir + + def remote_global_explorer_path(self, explorer): + """Returns path to the remote explorer""" + return os.path.join(REMOTE_GLOBAL_EXPLORER_DIR, explorer) + + def list_global_explorers(self): + """Return list of available explorers""" + return os.listdir(self.global_explorer_dir) + + def list_type_explorers(self, type): + """Return list of available explorers for a specific type""" + dir = self.type_dir(type, "explorer") + if os.path.isdir(dir): + list = os.listdir(dir) + else: + list = [] + + log.debug("Explorers for %s in %s: %s", type, dir, list) + + return list + + def list_types(self): + return os.listdir(self.type_base_dir) + + def list_object_paths(self, starting_point): + """Return list of paths of existing objects""" + object_paths = [] + + for content in os.listdir(starting_point): + full_path = os.path.join(starting_point, content) + if os.path.isdir(full_path): + object_paths.extend(self.list_object_paths(starting_point = full_path)) + + # Directory contains .cdist -> is an object + if content == DOT_CDIST: + object_paths.append(starting_point) + + return object_paths + + # FIXME + def get_type_from_object(self, cdist_object): + """Returns the first part (i.e. type) of an object""" + return cdist_object.split(os.sep)[0] + + def get_object_id_from_object(self, cdist_object): + """Returns everything but the first part (i.e. object_id) of an object""" + return os.sep.join(cdist_object.split(os.sep)[1:]) + + def object_dir(self, cdist_object): + """Returns the full path to the object (including .cdist)""" + return os.path.join(self.object_base_dir, cdist_object, DOT_CDIST) + + def remote_object_dir(self, cdist_object): + """Returns the remote full path to the object (including .cdist)""" + return os.path.join(REMOTE_OBJECT_DIR, cdist_object, DOT_CDIST) + + def object_parameter_dir(self, cdist_object): + """Returns the dir to the object parameter""" + return os.path.join(self.object_dir(cdist_object), "parameter") + + def remote_object_parameter_dir(self, cdist_object): + """Returns the remote dir to the object parameter""" + return os.path.join(self.remote_object_dir(cdist_object), "parameter") + + def object_code_paths(self, cdist_object): + """Return paths to code scripts of object""" + return [os.path.join(self.object_dir(cdist_object), "code-local"), + os.path.join(self.object_dir(cdist_object), "code-remote")] + + def list_objects(self): + """Return list of existing objects""" + + objects = [] + if os.path.isdir(self.object_base_dir): + object_paths = self.list_object_paths(self.object_base_dir) + + for path in object_paths: + objects.append(os.path.relpath(path, self.object_base_dir)) + + return objects + + def type_dir(self, type, *args): + """Return directory the type""" + return os.path.join(self.type_base_dir, type, *args) + + def remote_type_explorer_dir(self, type): + """Return remote directory that holds the explorers of a type""" + return os.path.join(REMOTE_TYPE_DIR, type, "explorer") + + def transfer_object_parameter(self, cdist_object): + """Transfer the object parameter to the remote destination""" + # Create base path before using mkdir -p + self.remote_mkdir(self.remote_object_parameter_dir(cdist_object)) + + # Synchronise parameter dir afterwards + self.transfer_dir(self.object_parameter_dir(cdist_object), + self.remote_object_parameter_dir(cdist_object)) + + def transfer_global_explorers(self): + """Transfer the global explorers""" + self.remote_mkdir(REMOTE_GLOBAL_EXPLORER_DIR) + self.transfer_dir(self.global_explorer_dir, REMOTE_GLOBAL_EXPLORER_DIR) + + def transfer_type_explorers(self, type): + """Transfer explorers of a type, but only once""" + if type in self.type_explorers_transferred: + log.debug("Skipping retransfer for explorers of %s", type) + return + else: + # Do not retransfer + self.type_explorers_transferred[type] = 1 + + src = self.type_dir(type, "explorer") + remote_base = os.path.join(REMOTE_TYPE_DIR, type) + dst = self.remote_type_explorer_dir(type) + + # Only continue, if there is at least the directory + if os.path.isdir(src): + # Ensure that the path exists + self.remote_mkdir(remote_base) + self.transfer_dir(src, dst) + + + def link_type_to_emulator(self): + """Link type names to cdist-type-emulator""" + source = os.path.abspath(sys.argv[0]) + for type in self.list_types(): + destination = os.path.join(self.bin_dir, type) + log.debug("Linking %s to %s", source, destination) + os.symlink(source, destination) diff --git a/test/cdist.py b/test/cdist.py new file mode 100644 index 00000000..3ccc69bc --- /dev/null +++ b/test/cdist.py @@ -0,0 +1,12 @@ +import cdist +import unittest + + +class CdistGeneric(unittest.TestCase): + + def test_initial_manifest(self): + self.assertEqual(numeral, result) + + +if __name__ == '__main__': + unittest.main()