From a20b7167cddc55abfad21d4fec1a43d50a7dd432 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Wed, 19 Jul 2017 07:58:14 +0200 Subject: [PATCH 01/10] pep8 --- cdist/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdist/config.py b/cdist/config.py index fccc93a0..450fde29 100644 --- a/cdist/config.py +++ b/cdist/config.py @@ -416,7 +416,7 @@ class Config(object): for chunk in cargo: for obj in chunk: if (obj.cdist_type == cdist_object.cdist_type and - cdist_object.cdist_type.is_nonparallel): + cdist_object.cdist_type.is_nonparallel): break else: chunk.append(cdist_object) From 4a72592ae5eda559851078b1779cb9bbde685511 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Thu, 20 Jul 2017 18:45:44 +0200 Subject: [PATCH 02/10] Document nonparallel type flag. --- docs/src/cdist-type.rst | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/src/cdist-type.rst b/docs/src/cdist-type.rst index 59423332..bfe3c35d 100644 --- a/docs/src/cdist-type.rst +++ b/docs/src/cdist-type.rst @@ -64,15 +64,23 @@ If a type is flagged with 'install' flag then it is used only with install comma With other commands, i.e. config, these types are skipped if used. +Nonparallel types +----------------- +If a type is flagged with 'nonparallel' flag then its objects cannot be run in parallel +when using -j option. Example of such a type is __package_dpkg type where dpkg itself +prevents to be run in more than one instance. + + How to write a new type ----------------------- A type consists of -- parameter (optional) -- manifest (optional) -- singleton (optional) -- explorer (optional) -- gencode (optional) +- parameter (optional) +- manifest (optional) +- singleton (optional) +- explorer (optional) +- gencode (optional) +- nonparallel (optional) Types are stored below cdist/conf/type/. Their name should always be prefixed with two underscores (__) to prevent collisions with other executables in $PATH. @@ -240,6 +248,19 @@ install: create the (empty) file "install" in your type directory: With other commands, i.e. config, it will be skipped if used. +Nonparallel - only one instance can be run at a time +---------------------------------------------------- +If objects of a type must not or cannot be run in parallel when using -j +option, you must mark it as nonparallel: create the (empty) file "nonparallel" +in your type directory: + +.. code-block:: sh + + touch cdist/conf/type/__NAME/nonparallel + +For example, package types are nonparallel types. + + The type explorers ------------------ If a type needs to explore specific details, it can provide type specific From 6a256213f11ba938c0a8e90438b625a32eb085c6 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Thu, 20 Jul 2017 20:08:15 +0200 Subject: [PATCH 03/10] Add missing -r option argument in cdist man page. --- docs/src/man1/cdist.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/man1/cdist.rst b/docs/src/man1/cdist.rst index edcad828..3ee10326 100644 --- a/docs/src/man1/cdist.rst +++ b/docs/src/man1/cdist.rst @@ -125,7 +125,7 @@ Configure/install one or more hosts. Operate on multiple hosts in parallel -.. option:: -r, --remote-out-dir +.. option:: -r REMOTE_OUT_PATH, --remote-out-dir REMOTE_OUT_PATH Directory to save cdist output in on the target host From b705893f3884e39dfd313aed5273bdf1b54b1271 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Thu, 20 Jul 2017 20:12:08 +0200 Subject: [PATCH 04/10] Add missing -q in cdist man page. --- docs/src/man1/cdist.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/man1/cdist.rst b/docs/src/man1/cdist.rst index 3ee10326..37ea4969 100644 --- a/docs/src/man1/cdist.rst +++ b/docs/src/man1/cdist.rst @@ -11,23 +11,23 @@ SYNOPSIS :: - cdist [-h] [-v] [-V] {banner,config,shell,install} ... + cdist [-h] [-q] [-v] [-V] {banner,config,shell,install} ... - cdist banner [-h] [-v] + cdist banner [-h] [-q] [-v] - cdist config [-h] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] + cdist config [-h] [-q] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] [-i MANIFEST] [-j [JOBS]] [-n] [-o OUT_PATH] [--remote-copy REMOTE_COPY] [--remote-exec REMOTE_EXEC] [-f HOSTFILE] [-p] [-r REMOTE_OUT_PATH] [-s] [host [host ...]] - cdist install [-h] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] + cdist install [-h] [-q] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] [-i MANIFEST] [-j [JOBS]] [-n] [-o OUT_PATH] [--remote-copy REMOTE_COPY] [--remote-exec REMOTE_EXEC] [-f HOSTFILE] [-p] [-r REMOTE_OUT_PATH] [-s] [host [host ...]] - cdist shell [-h] [-v] [-s SHELL] + cdist shell [-h] [-q] [-v] [-s SHELL] DESCRIPTION From 2b6177c9f74abb14f490e6be2e932ac3da86a5ab Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Thu, 20 Jul 2017 20:48:07 +0200 Subject: [PATCH 05/10] Fix rst. --- docs/src/cdist-best-practice.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/cdist-best-practice.rst b/docs/src/cdist-best-practice.rst index fdbb7a80..45aba11b 100644 --- a/docs/src/cdist-best-practice.rst +++ b/docs/src/cdist-best-practice.rst @@ -97,7 +97,7 @@ Including a possible common base that is reused across the different sites:: git merge common -The following **.git/config** is taken from a real world scenario: +The following **.git/config** is taken from a real world scenario:: # Track upstream, merge from time to time [remote "upstream"] From e2a1519332e176e3a3c2d48e5a17b373070f03b5 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Thu, 20 Jul 2017 22:04:44 +0200 Subject: [PATCH 06/10] Merge inventory from beta branch. --- .gitignore | 3 + cdist/argparse.py | 166 +++++- cdist/conf/type/__install_stage/man.rst | 6 +- cdist/config.py | 49 +- cdist/core/code.py | 3 + cdist/core/explorer.py | 1 + cdist/core/manifest.py | 2 + cdist/exec/local.py | 5 + cdist/inventory.py | 390 ++++++++++++++ cdist/shell.py | 3 + cdist/test/__init__.py | 1 + cdist/test/code/__init__.py | 5 + .../type/__dump_environment/gencode-local | 1 + cdist/test/config/__init__.py | 3 + cdist/test/emulator/__init__.py | 7 + cdist/test/explorer/__init__.py | 1 + cdist/test/inventory/__init__.py | 476 ++++++++++++++++++ cdist/test/manifest/__init__.py | 5 + .../fixtures/conf/manifest/dump_environment | 1 + .../conf/type/__dump_environment/manifest | 1 + completions/bash/cdist-completion.bash | 59 ++- completions/zsh/_cdist | 41 +- docs/changelog | 4 + docs/src/cdist-inventory.rst | 211 ++++++++ docs/src/cdist-reference.rst.sh | 10 + docs/src/index.rst | 1 + docs/src/man1/cdist.rst | 349 ++++++++++++- scripts/cdist | 1 + 28 files changed, 1769 insertions(+), 36 deletions(-) create mode 100644 cdist/inventory.py create mode 100644 cdist/test/inventory/__init__.py create mode 100644 docs/src/cdist-inventory.rst diff --git a/.gitignore b/.gitignore index 4258c2eb..55374d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ docs/src/cdist-reference.rst # Ignore cdist cache for version control /cache/ +# Ignore inventory basedir +cdist/inventory/ + # Python: cache, distutils, distribution in general __pycache__/ *.pyc diff --git a/cdist/argparse.py b/cdist/argparse.py index 8208d1f0..16b9d054 100644 --- a/cdist/argparse.py +++ b/cdist/argparse.py @@ -7,10 +7,10 @@ import collections # set of beta sub-commands -BETA_COMMANDS = set(('install', )) +BETA_COMMANDS = set(('install', 'inventory', )) # set of beta arguments for sub-commands BETA_ARGS = { - 'config': set(('jobs', )), + 'config': set(('jobs', 'tag', 'all_tagged_hosts', )), } EPILOG = "Get cdist at http://www.nico.schottelius.org/software/cdist/" # Parser others can reuse @@ -121,6 +121,17 @@ def get_parsers(): 'banner', parents=[parser['loglevel']]) parser['banner'].set_defaults(func=cdist.banner.banner) + parser['inventory_common'] = argparse.ArgumentParser(add_help=False) + parser['inventory_common'].add_argument( + '-I', '--inventory', + help=('Use specified custom inventory directory. ' + 'Inventory directory is set up by the following rules: ' + 'if this argument is set then specified directory is used, ' + 'if CDIST_INVENTORY_DIR env var is set then its value is ' + 'used, if HOME env var is set then ~/.cdist/inventory is ' + 'used, otherwise distribution inventory directory is used.'), + dest="inventory_dir", required=False) + # Config parser['config_main'] = argparse.ArgumentParser(add_help=False) parser['config_main'].add_argument( @@ -156,6 +167,10 @@ def get_parsers(): # remote-copy and remote-exec defaults are environment variables # if set; if not then None - these will be futher handled after # parsing to determine implementation default + parser['config_main'].add_argument( + '-r', '--remote-out-dir', + help='Directory to save cdist output in on the target host', + dest="remote_out_path") parser['config_main'].add_argument( '--remote-copy', help='Command to use for remote copy (should behave like scp)', @@ -170,6 +185,15 @@ def get_parsers(): # Config parser['config_args'] = argparse.ArgumentParser(add_help=False) + parser['config_args'].add_argument( + '-A', '--all-tagged', + help=('use all hosts present in tags db'), + action="store_true", dest="all_tagged_hosts", default=False) + parser['config_args'].add_argument( + '-a', '--all', + help=('list hosts that have all specified tags, ' + 'if -t/--tag is specified'), + action="store_true", dest="has_all_tags", default=False) parser['config_args'].add_argument( 'host', nargs='*', help='host(s) to operate on') parser['config_args'].add_argument( @@ -183,17 +207,19 @@ def get_parsers(): '-p', '--parallel', help='operate on multiple hosts in parallel', action='store_true', dest='parallel') - parser['config_args'].add_argument( - '-r', '--remote-out-dir', - help='Directory to save cdist output in on the target host', - dest="remote_out_path") parser['config_args'].add_argument( '-s', '--sequential', help='operate on multiple hosts sequentially (default)', action='store_false', dest='parallel') + parser['config_args'].add_argument( + '-t', '--tag', + help=('host is specified by tag, not hostname/address; ' + 'list all hosts that contain any of specified tags'), + dest='tag', required=False, action="store_true", default=False) parser['config'] = parser['sub'].add_parser( 'config', parents=[parser['loglevel'], parser['beta'], parser['config_main'], + parser['inventory_common'], parser['config_args']]) parser['config'].set_defaults(func=cdist.config.Config.commandline) @@ -202,6 +228,134 @@ def get_parsers(): parents=[parser['config']]) parser['install'].set_defaults(func=cdist.install.Install.commandline) + # Inventory + parser['inventory'] = parser['sub'].add_parser( + 'inventory', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['invsub'] = parser['inventory'].add_subparsers( + title="Inventory commands", dest="subcommand") + + parser['add-host'] = parser['invsub'].add_parser( + 'add-host', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['add-host'].add_argument( + 'host', nargs='*', help='host(s) to add') + parser['add-host'].add_argument( + '-f', '--file', + help=('Read additional hosts to add from specified file ' + 'or from stdin if \'-\' (each host on separate line). ' + 'If no host or host file is specified then, by default, ' + 'read from stdin.'), + dest='hostfile', required=False) + + parser['add-tag'] = parser['invsub'].add_parser( + 'add-tag', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['add-tag'].add_argument( + 'host', nargs='*', + help='list of host(s) for which tags are added') + parser['add-tag'].add_argument( + '-f', '--file', + help=('Read additional hosts to add tags from specified file ' + 'or from stdin if \'-\' (each host on separate line). ' + 'If no host or host file is specified then, by default, ' + 'read from stdin. If no tags/tagfile nor hosts/hostfile' + ' are specified then tags are read from stdin and are' + ' added to all hosts.'), + dest='hostfile', required=False) + parser['add-tag'].add_argument( + '-T', '--tag-file', + help=('Read additional tags to add from specified file ' + 'or from stdin if \'-\' (each tag on separate line). ' + 'If no tag or tag file is specified then, by default, ' + 'read from stdin. If no tags/tagfile nor hosts/hostfile' + ' are specified then tags are read from stdin and are' + ' added to all hosts.'), + dest='tagfile', required=False) + parser['add-tag'].add_argument( + '-t', '--taglist', + help=("Tag list to be added for specified host(s), comma separated" + " values"), + dest="taglist", required=False) + + parser['del-host'] = parser['invsub'].add_parser( + 'del-host', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['del-host'].add_argument( + 'host', nargs='*', help='host(s) to delete') + parser['del-host'].add_argument( + '-a', '--all', help=('Delete all hosts'), + dest='all', required=False, action="store_true", default=False) + parser['del-host'].add_argument( + '-f', '--file', + help=('Read additional hosts to delete from specified file ' + 'or from stdin if \'-\' (each host on separate line). ' + 'If no host or host file is specified then, by default, ' + 'read from stdin.'), + dest='hostfile', required=False) + + parser['del-tag'] = parser['invsub'].add_parser( + 'del-tag', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['del-tag'].add_argument( + 'host', nargs='*', + help='list of host(s) for which tags are deleted') + parser['del-tag'].add_argument( + '-a', '--all', + help=('Delete all tags for specified host(s)'), + dest='all', required=False, action="store_true", default=False) + parser['del-tag'].add_argument( + '-f', '--file', + help=('Read additional hosts to delete tags for from specified ' + 'file or from stdin if \'-\' (each host on separate line). ' + 'If no host or host file is specified then, by default, ' + 'read from stdin. If no tags/tagfile nor hosts/hostfile' + ' are specified then tags are read from stdin and are' + ' deleted from all hosts.'), + dest='hostfile', required=False) + parser['del-tag'].add_argument( + '-T', '--tag-file', + help=('Read additional tags from specified file ' + 'or from stdin if \'-\' (each tag on separate line). ' + 'If no tag or tag file is specified then, by default, ' + 'read from stdin. If no tags/tagfile nor' + ' hosts/hostfile are specified then tags are read from' + ' stdin and are added to all hosts.'), + dest='tagfile', required=False) + parser['del-tag'].add_argument( + '-t', '--taglist', + help=("Tag list to be deleted for specified host(s), " + "comma separated values"), + dest="taglist", required=False) + + parser['list'] = parser['invsub'].add_parser( + 'list', parents=[parser['loglevel'], parser['beta'], + parser['inventory_common']]) + parser['list'].add_argument( + 'host', nargs='*', help='host(s) to list') + parser['list'].add_argument( + '-a', '--all', + help=('list hosts that have all specified tags, ' + 'if -t/--tag is specified'), + action="store_true", dest="has_all_tags", default=False) + parser['list'].add_argument( + '-f', '--file', + help=('Read additional hosts to list from specified file ' + 'or from stdin if \'-\' (each host on separate line). ' + 'If no host or host file is specified then, by default, ' + 'list all.'), dest='hostfile', required=False) + parser['list'].add_argument( + '-H', '--host-only', help=('Suppress tags listing'), + action="store_true", dest="list_only_host", default=False) + parser['list'].add_argument( + '-t', '--tag', + help=('host is specified by tag, not hostname/address; ' + 'list all hosts that contain any of specified tags'), + action="store_true", default=False) + + parser['inventory'].set_defaults( + func=cdist.inventory.Inventory.commandline) + # Shell parser['shell'] = parser['sub'].add_parser( 'shell', parents=[parser['loglevel']]) diff --git a/cdist/conf/type/__install_stage/man.rst b/cdist/conf/type/__install_stage/man.rst index 6c68c543..e33e1e90 100644 --- a/cdist/conf/type/__install_stage/man.rst +++ b/cdist/conf/type/__install_stage/man.rst @@ -17,9 +17,9 @@ REQUIRED PARAMETERS uri The uri from which to fetch the tarball. Can be anything understood by curl, e.g: - | http://path/to/stage.tgz - | tftp:///path/to/stage.tgz - | file:///local/path/stage.tgz + | http://path/to/stage.tgz + | tftp:///path/to/stage.tgz + | file:///local/path/stage.tgz OPTIONAL PARAMETERS diff --git a/cdist/config.py b/cdist/config.py index 450fde29..2c9721f5 100644 --- a/cdist/config.py +++ b/cdist/config.py @@ -38,6 +38,9 @@ import cdist.hostsource import cdist.exec.local import cdist.exec.remote + +from cdist import inventory + import cdist.util.ipaddr as ipaddr from cdist import core @@ -134,11 +137,42 @@ class Config(object): base_root_path = cls.create_base_root_path(args.out_path) hostcnt = 0 - for host in itertools.chain(cls.hosts(args.host), - cls.hosts(args.hostfile)): + + if args.tag or args.all_tagged_hosts: + inventory.determine_default_inventory_dir(args) + if args.all_tagged_hosts: + inv_list = inventory.InventoryList( + hosts=None, istag=True, hostfile=None, + db_basedir=args.inventory_dir) + else: + inv_list = inventory.InventoryList( + hosts=args.host, istag=True, hostfile=args.hostfile, + db_basedir=args.inventory_dir, + has_all_tags=args.has_all_tags) + it = inv_list.entries() + else: + it = itertools.chain(cls.hosts(args.host), + cls.hosts(args.hostfile)) + for entry in it: + if isinstance(entry, tuple): + # if configuring by specified tags + host = entry[0] + host_tags = entry[1] + else: + # if configuring by host then check inventory for tags + host = entry + inventory.determine_default_inventory_dir(args) + inv_list = inventory.InventoryList( + hosts=(host,), db_basedir=args.inventory_dir) + inv = tuple(inv_list.entries()) + if inv: + # host is present in inventory and has tags + host_tags = inv[0][1] + else: + # host is not present in inventory or has no tags + host_tags = None host_base_path, hostdir = cls.create_host_base_dirs( host, base_root_path) - log.debug("Base root path for target host \"{}\" is \"{}\"".format( host, host_base_path)) @@ -147,11 +181,12 @@ class Config(object): log.trace("Creating child process for %s", host) process[host] = multiprocessing.Process( target=cls.onehost, - args=(host, host_base_path, hostdir, args, True)) + args=(host, host_tags, host_base_path, hostdir, args, + True)) process[host].start() else: try: - cls.onehost(host, host_base_path, hostdir, + cls.onehost(host, host_tags, host_base_path, hostdir, args, parallel=False) except cdist.Error as e: failed_hosts.append(host) @@ -199,7 +234,8 @@ class Config(object): return (remote_exec, remote_copy, ) @classmethod - def onehost(cls, host, host_base_path, host_dir_name, args, parallel): + def onehost(cls, host, host_tags, host_base_path, host_dir_name, args, + parallel): """Configure ONE system""" log = logging.getLogger(host) @@ -216,6 +252,7 @@ class Config(object): local = cdist.exec.local.Local( target_host=target_host, + target_host_tags=host_tags, base_root_path=host_base_path, host_dir_name=host_dir_name, initial_manifest=args.manifest, diff --git a/cdist/core/code.py b/cdist/core/code.py index 1b5fe1d6..173d192d 100644 --- a/cdist/core/code.py +++ b/cdist/core/code.py @@ -56,6 +56,7 @@ gencode-local __object_fq: full qualified object id, iow: $type.name + / + object_id __type: full qualified path to the type's dir __files: full qualified path to the files dir + __target_host_tags: comma spearated list of host tags returns: string containing the generated code or None @@ -74,6 +75,7 @@ gencode-remote __object_fq: full qualified object id, iow: $type.name + / + object_id __type: full qualified path to the type's dir __files: full qualified path to the files dir + __target_host_tags: comma spearated list of host tags returns: string containing the generated code or None @@ -106,6 +108,7 @@ class Code(object): '__target_fqdn': self.target_host[2], '__global': self.local.base_path, '__files': self.local.files_path, + '__target_host_tags': self.local.target_host_tags, } def _run_gencode(self, cdist_object, which): diff --git a/cdist/core/explorer.py b/cdist/core/explorer.py index d604c015..38f2a921 100644 --- a/cdist/core/explorer.py +++ b/cdist/core/explorer.py @@ -77,6 +77,7 @@ class Explorer(object): '__target_hostname': self.target_host[1], '__target_fqdn': self.target_host[2], '__explorer': self.remote.global_explorer_path, + '__target_host_tags': self.local.target_host_tags, } self._type_explorers_transferred = [] self.jobs = jobs diff --git a/cdist/core/manifest.py b/cdist/core/manifest.py index d8570097..6f941550 100644 --- a/cdist/core/manifest.py +++ b/cdist/core/manifest.py @@ -42,6 +42,7 @@ common: types are defined for use in type emulator == local.type_path __files: full qualified path to the files dir + __target_host_tags: comma spearated list of host tags initial manifest is: script: full qualified path to the initial manifest @@ -109,6 +110,7 @@ class Manifest(object): '__target_hostname': self.target_host[1], '__target_fqdn': self.target_host[2], '__files': self.local.files_path, + '__target_host_tags': self.local.target_host_tags, } if self.log.getEffectiveLevel() == logging.DEBUG: diff --git a/cdist/exec/local.py b/cdist/exec/local.py index 6c285204..23ad4ce9 100644 --- a/cdist/exec/local.py +++ b/cdist/exec/local.py @@ -49,6 +49,7 @@ class Local(object): """ def __init__(self, target_host, + target_host_tags, base_root_path, host_dir_name, exec_path=sys.argv[0], @@ -58,6 +59,10 @@ class Local(object): quiet_mode=False): self.target_host = target_host + if target_host_tags is None: + self.target_host_tags = "" + else: + self.target_host_tags = ",".join(target_host_tags) self.hostdir = host_dir_name self.base_path = os.path.join(base_root_path, "data") diff --git a/cdist/inventory.py b/cdist/inventory.py new file mode 100644 index 00000000..ccb4428f --- /dev/null +++ b/cdist/inventory.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# 2016 Darko Poljak (darko.poljak at gmail.com) +# +# 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 cdist +import logging +import os +import os.path +import itertools +import sys +from cdist.hostsource import hostfile_process_line + +DIST_INVENTORY_DB_NAME = "inventory" + +dist_inventory_db = os.path.abspath(os.path.join( + os.path.dirname(cdist.__file__), DIST_INVENTORY_DB_NAME)) + + +def determine_default_inventory_dir(args): + # The order of inventory dir setting by decreasing priority + # 1. inventory_dir argument + # 2. CDIST_INVENTORY_DIR env var if set + # 3. ~/.cdist/inventory if HOME env var is set + # 4. distribution inventory directory + if not args.inventory_dir: + if 'CDIST_INVENTORY_DIR' in os.environ: + args.inventory_dir = os.environ['CDIST_INVENTORY_DIR'] + else: + home = cdist.home_dir() + if home: + args.inventory_dir = os.path.join(home, DIST_INVENTORY_DB_NAME) + else: + args.inventory_dir = dist_inventory_db + + +def contains_all(big, little): + """Return True if big contains all elements from little, + False otherwise. + """ + return set(little).issubset(set(big)) + + +def contains_any(big, little): + """Return True if big contains any element from little, + False otherwise. + """ + for x in little: + if x in big: + return True + return False + + +def check_always_true(x, y): + return True + + +def rstrip_nl(s): + '''str.rstrip "\n" from s''' + return str.rstrip(s, "\n") + + +class Inventory(object): + """Inventory main class""" + + def __init__(self, db_basedir=dist_inventory_db): + self.db_basedir = db_basedir + self.log = logging.getLogger("inventory") + self.init_db() + + def init_db(self): + self.log.debug("Init db: {}".format(self.db_basedir)) + if not os.path.exists(self.db_basedir): + os.makedirs(self.db_basedir, exist_ok=True) + elif not os.path.isdir(self.db_basedir): + raise cdist.Error(("Invalid inventory db basedir \'{}\'," + " must be a directory").format(self.db_basedir)) + + @staticmethod + def strlist_to_list(slist): + if slist: + result = [x for x in slist.split(',') if x] + else: + result = [] + return result + + def _input_values(self, source): + """Yield input values from source. + Source can be a sequence or filename (stdin if '-'). + In case of filename each line represents one input value. + """ + if isinstance(source, str): + import fileinput + try: + with fileinput.FileInput(files=(source)) as f: + for x in f: + result = hostfile_process_line(x, strip_func=rstrip_nl) + if result: + yield result + except (IOError, OSError) as e: + raise cdist.Error("Error reading from \'{}\'".format( + source)) + else: + if source: + for x in source: + if x: + yield x + + def _host_path(self, host): + hostpath = os.path.join(self.db_basedir, host) + return hostpath + + def _all_hosts(self): + return os.listdir(self.db_basedir) + + def _check_host(self, hostpath): + if not os.path.exists(hostpath): + return False + else: + if not os.path.isfile(hostpath): + raise cdist.Error(("Host path \'{}\' exists, but is not" + " a valid file").format(hostpath)) + return True + + def _read_host_tags(self, hostpath): + result = set() + with open(hostpath, "rt") as f: + for tag in f: + tag = tag.rstrip("\n") + if tag: + result.add(tag) + return result + + def _get_host_tags(self, host): + hostpath = self._host_path(host) + if self._check_host(hostpath): + return self._read_host_tags(hostpath) + else: + return None + + def _write_host_tags(self, host, tags): + hostpath = self._host_path(host) + if self._check_host(hostpath): + with open(hostpath, "wt") as f: + for tag in tags: + f.write("{}\n".format(tag)) + return True + else: + return False + + @classmethod + def commandline(cls, args): + """Manipulate inventory db""" + log = logging.getLogger("cdist") + if 'taglist' in args: + args.taglist = cls.strlist_to_list(args.taglist) + determine_default_inventory_dir(args) + + log.info("Using inventory: {}".format(args.inventory_dir)) + log.debug("Inventory args: {}".format(vars(args))) + log.debug("Inventory command: {}".format(args.subcommand)) + + if args.subcommand == "list": + c = InventoryList(hosts=args.host, istag=args.tag, + hostfile=args.hostfile, + db_basedir=args.inventory_dir, + list_only_host=args.list_only_host, + has_all_tags=args.has_all_tags) + elif args.subcommand == "add-host": + c = InventoryHost(hosts=args.host, hostfile=args.hostfile, + db_basedir=args.inventory_dir) + elif args.subcommand == "del-host": + c = InventoryHost(hosts=args.host, hostfile=args.hostfile, + all=args.all, db_basedir=args.inventory_dir, + action="del") + elif args.subcommand == "add-tag": + c = InventoryTag(hosts=args.host, tags=args.taglist, + hostfile=args.hostfile, tagfile=args.tagfile, + db_basedir=args.inventory_dir) + elif args.subcommand == "del-tag": + c = InventoryTag(hosts=args.host, tags=args.taglist, + hostfile=args.hostfile, tagfile=args.tagfile, + all=args.all, db_basedir=args.inventory_dir, + action="del") + else: + raise cdist.Error("Unknown inventory command \'{}\'".format( + args.subcommand)) + c.run() + + +class InventoryList(Inventory): + def __init__(self, hosts=None, istag=False, hostfile=None, + list_only_host=False, has_all_tags=False, + db_basedir=dist_inventory_db): + super().__init__(db_basedir) + self.hosts = hosts + self.istag = istag + self.hostfile = hostfile + self.list_only_host = list_only_host + self.has_all_tags = has_all_tags + + def _print(self, host, tags): + if self.list_only_host: + print("{}".format(host)) + else: + print("{} {}".format(host, ",".join(sorted(tags)))) + + def _do_list(self, it_tags, it_hosts, check_func): + if (it_tags is not None): + param_tags = set(it_tags) + self.log.debug("param_tags: {}".format(param_tags)) + else: + param_tags = set() + for host in it_hosts: + self.log.debug("host: {}".format(host)) + tags = self._get_host_tags(host) + if tags is None: + self.log.info("Host \'{}\' not found, skipped".format(host)) + continue + self.log.debug("tags: {}".format(tags)) + if check_func(tags, param_tags): + yield host, tags + + def entries(self): + if not self.hosts and not self.hostfile: + self.log.info("Listing all hosts") + it_hosts = self._all_hosts() + it_tags = None + check_func = check_always_true + else: + it = itertools.chain(self._input_values(self.hosts), + self._input_values(self.hostfile)) + if self.istag: + self.log.info("Listing by tag(s)") + it_hosts = self._all_hosts() + it_tags = it + if self.has_all_tags: + check_func = contains_all + else: + check_func = contains_any + else: + self.log.info("Listing by host(s)") + it_hosts = it + it_tags = None + check_func = check_always_true + for host, tags in self._do_list(it_tags, it_hosts, check_func): + yield host, tags + + def host_entries(self): + for host, tags in self.entries(): + yield host + + def run(self): + for host, tags in self.entries(): + self._print(host, tags) + + +class InventoryHost(Inventory): + def __init__(self, hosts=None, hostfile=None, + db_basedir=dist_inventory_db, all=False, action="add"): + super().__init__(db_basedir) + self.actions = ("add", "del") + if action not in self.actions: + raise cdist.Error("Invalid action \'{}\', valid actions are:" + " {}\n".format(action, self.actions.keys())) + self.action = action + self.hosts = hosts + self.hostfile = hostfile + self.all = all + + if not self.hosts and not self.hostfile: + self.hostfile = "-" + + def _new_hostpath(self, hostpath): + # create empty file + with open(hostpath, "w"): + pass + + def _action(self, host): + if self.action == "add": + self.log.info("Adding host \'{}\'".format(host)) + elif self.action == "del": + self.log.info("Deleting host \'{}\'".format(host)) + hostpath = self._host_path(host) + self.log.debug("hostpath: {}".format(hostpath)) + if self.action == "add" and not os.path.exists(hostpath): + self._new_hostpath(hostpath) + else: + if not os.path.isfile(hostpath): + raise cdist.Error(("Host path \'{}\' is" + " not a valid file").format(hostpath)) + if self.action == "del": + os.remove(hostpath) + + def run(self): + if self.action == "del" and self.all: + self.log.debug("Doing for all hosts") + it = self._all_hosts() + else: + self.log.debug("Doing for specified hosts") + it = itertools.chain(self._input_values(self.hosts), + self._input_values(self.hostfile)) + for host in it: + self._action(host) + + +class InventoryTag(Inventory): + def __init__(self, hosts=None, tags=None, hostfile=None, tagfile=None, + db_basedir=dist_inventory_db, all=False, action="add"): + super().__init__(db_basedir) + self.actions = ("add", "del") + if action not in self.actions: + raise cdist.Error("Invalid action \'{}\', valid actions are:" + " {}\n".format(action, self.actions.keys())) + self.action = action + self.hosts = hosts + self.tags = tags + self.hostfile = hostfile + self.tagfile = tagfile + self.all = all + + if not self.hosts and not self.hostfile: + self.allhosts = True + else: + self.allhosts = False + if not self.tags and not self.tagfile: + self.tagfile = "-" + + if self.hostfile == "-" and self.tagfile == "-": + raise cdist.Error("Cannot read both, hosts and tags, from stdin") + + def _read_input_tags(self): + self.input_tags = set() + for tag in itertools.chain(self._input_values(self.tags), + self._input_values(self.tagfile)): + self.input_tags.add(tag) + + def _action(self, host): + host_tags = self._get_host_tags(host) + if host_tags is None: + print("Host \'{}\' does not exist, skipping".format(host), + file=sys.stderr) + return + self.log.debug("existing host_tags: {}".format(host_tags)) + if self.action == "del" and self.all: + host_tags = set() + else: + for tag in self.input_tags: + if self.action == "add": + self.log.info("Adding tag \'{}\' for host \'{}\'".format( + tag, host)) + host_tags.add(tag) + elif self.action == "del": + self.log.info("Deleting tag \'{}\' for host \'{}\'".format( + tag, host)) + if tag in host_tags: + host_tags.remove(tag) + self.log.debug("new host tags: {}".format(host_tags)) + if not self._write_host_tags(host, host_tags): + self.log.info("{} does not exist, skipped".format(host)) + + def run(self): + if self.allhosts: + self.log.debug("Doing for all hosts") + it = self._all_hosts() + else: + self.log.debug("Doing for specified hosts") + it = itertools.chain(self._input_values(self.hosts), + self._input_values(self.hostfile)) + if not(self.action == "del" and self.all): + self._read_input_tags() + for host in it: + self._action(host) diff --git a/cdist/shell.py b/cdist/shell.py index 662f8f7d..44cdd49d 100644 --- a/cdist/shell.py +++ b/cdist/shell.py @@ -44,6 +44,7 @@ class Shell(object): "cdist-shell-no-target-host", "cdist-shell-no-target-host", ) + self.target_host_tags = "" host_dir_name = cdist.str_hash(self.target_host[0]) base_root_path = tempfile.mkdtemp() @@ -51,6 +52,7 @@ class Shell(object): self.local = cdist.exec.local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=host_dir_name) @@ -77,6 +79,7 @@ class Shell(object): '__manifest': self.local.manifest_path, '__explorer': self.local.global_explorer_path, '__files': self.local.files_path, + '__target_host_tags': self.local.target_host_tags, } self.env.update(additional_env) diff --git a/cdist/test/__init__.py b/cdist/test/__init__.py index 83b0c618..faa3686a 100644 --- a/cdist/test/__init__.py +++ b/cdist/test/__init__.py @@ -42,6 +42,7 @@ class CdistTestCase(unittest.TestCase): 'cdisttesthost', 'cdisttesthost', ) + target_host_tags = "tag1,tag2,tag3" def mkdtemp(self, **kwargs): return tempfile.mkdtemp(prefix='tmp.cdist.test.', **kwargs) diff --git a/cdist/test/code/__init__.py b/cdist/test/code/__init__.py index 83c93f8b..50da2b8a 100644 --- a/cdist/test/code/__init__.py +++ b/cdist/test/code/__init__.py @@ -46,6 +46,7 @@ class CodeTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=self.host_base_path, host_dir_name=self.hostdir, exec_path=cdist.test.cdist_exec_path, @@ -97,6 +98,8 @@ class CodeTestCase(test.CdistTestCase): self.cdist_object.object_id) self.assertEqual(output_dict['__object_name'], self.cdist_object.name) self.assertEqual(output_dict['__files'], self.local.files_path) + self.assertEqual(output_dict['__target_host_tags'], + self.local.target_host_tags) def test_run_gencode_remote_environment(self): output_string = self.code.run_gencode_remote(self.cdist_object) @@ -120,6 +123,8 @@ class CodeTestCase(test.CdistTestCase): self.cdist_object.object_id) self.assertEqual(output_dict['__object_name'], self.cdist_object.name) self.assertEqual(output_dict['__files'], self.local.files_path) + self.assertEqual(output_dict['__target_host_tags'], + self.local.target_host_tags) def test_transfer_code_remote(self): self.cdist_object.code_remote = self.code.run_gencode_remote( diff --git a/cdist/test/code/fixtures/conf/type/__dump_environment/gencode-local b/cdist/test/code/fixtures/conf/type/__dump_environment/gencode-local index 7fa70342..56744a27 100755 --- a/cdist/test/code/fixtures/conf/type/__dump_environment/gencode-local +++ b/cdist/test/code/fixtures/conf/type/__dump_environment/gencode-local @@ -9,3 +9,4 @@ echo "echo __object: $__object" echo "echo __object_id: $__object_id" echo "echo __object_name: $__object_name" echo "echo __files: $__files" +echo "echo __target_host_tags: $__target_host_tags" diff --git a/cdist/test/config/__init__.py b/cdist/test/config/__init__.py index af1aa38f..5ff98269 100644 --- a/cdist/test/config/__init__.py +++ b/cdist/test/config/__init__.py @@ -60,6 +60,7 @@ class ConfigRunTestCase(test.CdistTestCase): os.makedirs(self.host_base_path) self.local = cdist.exec.local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=self.host_base_path, host_dir_name=self.hostdir) @@ -164,6 +165,7 @@ class ConfigRunTestCase(test.CdistTestCase): """Test if the dryrun option is working like expected""" drylocal = cdist.exec.local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=self.host_base_path, host_dir_name=self.hostdir, # exec_path can not derivated from sys.argv in case of unittest @@ -181,6 +183,7 @@ class ConfigRunTestCase(test.CdistTestCase): """Test to show dependency resolver warning message.""" local = cdist.exec.local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=self.host_base_path, host_dir_name=self.hostdir, exec_path=os.path.abspath(os.path.join( diff --git a/cdist/test/emulator/__init__.py b/cdist/test/emulator/__init__.py index 51de3180..664ab20b 100644 --- a/cdist/test/emulator/__init__.py +++ b/cdist/test/emulator/__init__.py @@ -53,6 +53,7 @@ class EmulatorTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -156,6 +157,7 @@ class EmulatorConflictingRequirementsTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -246,6 +248,7 @@ class AutoRequireEmulatorTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -279,6 +282,7 @@ class OverrideTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -322,6 +326,7 @@ class ArgumentsTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -445,6 +450,7 @@ class StdinTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, @@ -511,6 +517,7 @@ class EmulatorAlreadyExistingRequirementsWarnTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=host_base_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, diff --git a/cdist/test/explorer/__init__.py b/cdist/test/explorer/__init__.py index fc66020d..928b4e0d 100644 --- a/cdist/test/explorer/__init__.py +++ b/cdist/test/explorer/__init__.py @@ -50,6 +50,7 @@ class ExplorerClassTestCase(test.CdistTestCase): self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=base_root_path, host_dir_name=hostdir, exec_path=test.cdist_exec_path, diff --git a/cdist/test/inventory/__init__.py b/cdist/test/inventory/__init__.py new file mode 100644 index 00000000..4c0dd936 --- /dev/null +++ b/cdist/test/inventory/__init__.py @@ -0,0 +1,476 @@ +# -*- coding: utf-8 -*- +# +# 2016 Darko Poljak (darko.poljak at gmail.com) +# +# 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 os +import shutil +import cdist +import os.path as op +import unittest +import sys +from cdist import test +from cdist import inventory +from io import StringIO + +my_dir = op.abspath(op.dirname(__file__)) +fixtures = op.join(my_dir, 'fixtures') +inventory_dir = op.join(fixtures, "inventory") + + +class InventoryTestCase(test.CdistTestCase): + + def _create_host_with_tags(self, host, tags): + os.makedirs(inventory_dir, exist_ok=True) + hostfile = op.join(inventory_dir, host) + with open(hostfile, "w") as f: + for x in tags: + f.write("{}\n".format(x)) + + def setUp(self): + self.maxDiff = None + self.db = { + "loadbalancer1": ["loadbalancer", "all", "europe", ], + "loadbalancer2": ["loadbalancer", "all", "europe", ], + "loadbalancer3": ["loadbalancer", "all", "africa", ], + "loadbalancer4": ["loadbalancer", "all", "africa", ], + "web1": ["web", "all", "static", ], + "web2": ["web", "all", "dynamic", ], + "web3": ["web", "all", "dynamic", ], + "shell1": ["shell", "all", "free", ], + "shell2": ["shell", "all", "free", ], + "shell3": ["shell", "all", "charge", ], + "shell4": ["shell", "all", "charge", ], + "monty": ["web", "python", "shell", ], + "python": ["web", "python", "shell", ], + } + for x in self.db: + self.db[x] = sorted(self.db[x]) + for host in self.db: + self._create_host_with_tags(host, self.db[host]) + self.sys_stdout = sys.stdout + out = StringIO() + sys.stdout = out + + def _get_output(self): + sys.stdout.flush() + output = sys.stdout.getvalue().strip() + return output + + def tearDown(self): + sys.stdout = self.sys_stdout + shutil.rmtree(inventory_dir) + + def test_inventory_create_db(self): + dbdir = op.join(fixtures, "foo") + inv = inventory.Inventory(db_basedir=dbdir) + self.assertTrue(os.path.isdir(dbdir)) + self.assertEqual(inv.db_basedir, dbdir) + shutil.rmtree(inv.db_basedir) + + # InventoryList + def test_inventory_list_print(self): + invList = inventory.InventoryList(db_basedir=inventory_dir) + invList.run() + output = self._get_output() + self.assertTrue(' ' in output) + + def test_inventory_list_print_host_only(self): + invList = inventory.InventoryList(db_basedir=inventory_dir, + list_only_host=True) + invList.run() + output = self._get_output() + self.assertFalse(' ' in output) + + def test_inventory_list_all(self): + invList = inventory.InventoryList(db_basedir=inventory_dir) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + self.assertEqual(db, self.db) + + def test_inventory_list_by_host_hosts(self): + hosts = ("web1", "web2", "web3",) + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + expected_db = {host: sorted(self.db[host]) for host in hosts} + self.assertEqual(db, expected_db) + + def test_inventory_list_by_host_hostfile(self): + hosts = ("web1", "web2", "web3",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hosts: + f.write("{}\n".format(x)) + invList = inventory.InventoryList(db_basedir=inventory_dir, + hostfile=hostfile) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + expected_db = {host: sorted(self.db[host]) for host in hosts} + self.assertEqual(db, expected_db) + os.remove(hostfile) + + def test_inventory_list_by_host_hosts_hostfile(self): + hosts = ("shell1", "shell4",) + hostsf = ("web1", "web2", "web3",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostsf: + f.write("{}\n".format(x)) + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts, hostfile=hostfile) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + import itertools + expected_db = {host: sorted(self.db[host]) for host in + itertools.chain(hostsf, hosts)} + self.assertEqual(db, expected_db) + os.remove(hostfile) + + def _gen_expected_db_for_tags(self, tags): + db = {} + for host in self.db: + for tag in tags: + if tag in self.db[host]: + db[host] = self.db[host] + break + return db + + def _gen_expected_db_for_has_all_tags(self, tags): + db = {} + for host in self.db: + if set(tags).issubset(set(self.db[host])): + db[host] = self.db[host] + return db + + def test_inventory_list_by_tag_hosts(self): + tags = ("web", "shell",) + invList = inventory.InventoryList(db_basedir=inventory_dir, + istag=True, hosts=tags) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + expected_db = self._gen_expected_db_for_tags(tags) + self.assertEqual(db, expected_db) + + def test_inventory_list_by_tag_hostfile(self): + tags = ("web", "shell",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tags: + f.write("{}\n".format(x)) + invList = inventory.InventoryList(db_basedir=inventory_dir, + istag=True, hostfile=tagfile) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + expected_db = self._gen_expected_db_for_tags(tags) + self.assertEqual(db, expected_db) + os.remove(tagfile) + + def test_inventory_list_by_tag_hosts_hostfile(self): + tags = ("web", "shell",) + tagsf = ("dynamic", "europe",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tagsf: + f.write("{}\n".format(x)) + invList = inventory.InventoryList(db_basedir=inventory_dir, + istag=True, hosts=tags, + hostfile=tagfile) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + import itertools + expected_db = self._gen_expected_db_for_tags(tags + tagsf) + self.assertEqual(db, expected_db) + os.remove(tagfile) + + def test_inventory_list_by_tag_has_all_tags(self): + tags = ("web", "python", "shell",) + invList = inventory.InventoryList(db_basedir=inventory_dir, + istag=True, hosts=tags, + has_all_tags=True) + entries = invList.entries() + db = {host: sorted(tags) for host, tags in entries} + expected_db = self._gen_expected_db_for_has_all_tags(tags) + self.assertEqual(db, expected_db) + + # InventoryHost + def test_inventory_host_add_hosts(self): + hosts = ("spam", "eggs", "foo",) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="add", hosts=hosts) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir) + expected_hosts = tuple(x for x in invList.host_entries() if x in hosts) + self.assertEqual(sorted(hosts), sorted(expected_hosts)) + + def test_inventory_host_add_hostfile(self): + hosts = ("spam-new", "eggs-new", "foo-new",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hosts: + f.write("{}\n".format(x)) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="add", hostfile=hostfile) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir) + expected_hosts = tuple(x for x in invList.host_entries() if x in hosts) + self.assertEqual(sorted(hosts), sorted(expected_hosts)) + os.remove(hostfile) + + def test_inventory_host_add_hosts_hostfile(self): + hosts = ("spam-spam", "eggs-spam", "foo-spam",) + hostf = ("spam-eggs-spam", "spam-foo-spam",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostf: + f.write("{}\n".format(x)) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="add", hosts=hosts, + hostfile=hostfile) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts + hostf) + expected_hosts = tuple(invList.host_entries()) + self.assertEqual(sorted(hosts + hostf), sorted(expected_hosts)) + os.remove(hostfile) + + def test_inventory_host_del_hosts(self): + hosts = ("web1", "shell1",) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="del", hosts=hosts) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts) + expected_hosts = tuple(invList.host_entries()) + self.assertTupleEqual(expected_hosts, ()) + + def test_inventory_host_del_hostfile(self): + hosts = ("loadbalancer3", "loadbalancer4",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hosts: + f.write("{}\n".format(x)) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="del", hostfile=hostfile) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts) + expected_hosts = tuple(invList.host_entries()) + self.assertTupleEqual(expected_hosts, ()) + os.remove(hostfile) + + def test_inventory_host_del_hosts_hostfile(self): + hosts = ("loadbalancer1", "loadbalancer2",) + hostf = ("web2", "shell2",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostf: + f.write("{}\n".format(x)) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="del", hosts=hosts, + hostfile=hostfile) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts + hostf) + expected_hosts = tuple(invList.host_entries()) + self.assertTupleEqual(expected_hosts, ()) + os.remove(hostfile) + + @unittest.expectedFailure + def test_inventory_host_invalid_host(self): + try: + invalid_hostfile = op.join(inventory_dir, "invalid") + os.mkdir(invalid_hostfile) + hosts = ("invalid",) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="del", hosts=hosts) + invHost.run() + except e: + os.rmdir(invalid_hostfile) + raise e + + # InventoryTag + def test_inventory_tag_init(self): + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add") + self.assertTrue(invTag.allhosts) + self.assertEqual(invTag.tagfile, "-") + + def test_inventory_tag_stdin_multiple_hosts(self): + try: + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add", tagfile="-", + hosts=("host1", "host2",)) + except e: + self.fail() + + def test_inventory_tag_stdin_hostfile(self): + try: + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add", tagfile="-", + hostfile="hosts") + except e: + self.fail() + + @unittest.expectedFailure + def test_inventory_tag_stdin_both(self): + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add", tagfile="-", + hostfile="-") + + def test_inventory_tag_add_for_all_hosts(self): + tags = ("spam-spam-spam", "spam-spam-eggs",) + tagsf = ("spam-spam-spam-eggs", "spam-spam-eggs-spam",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tagsf: + f.write("{}\n".format(x)) + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add", tags=tags, + tagfile=tagfile) + invTag.run() + invList = inventory.InventoryList(db_basedir=inventory_dir) + failed = False + for host, taglist in invList.entries(): + for x in tagsf + tags: + if x not in taglist: + failed = True + break + if failed: + break + os.remove(tagfile) + if failed: + self.fail() + + def test_inventory_tag_add(self): + tags = ("spam-spam-spam", "spam-spam-eggs",) + tagsf = ("spam-spam-spam-eggs", "spam-spam-eggs-spam",) + hosts = ("loadbalancer1", "loadbalancer2", "shell2",) + hostsf = ("web2", "web3",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tagsf: + f.write("{}\n".format(x)) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostsf: + f.write("{}\n".format(x)) + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="add", tags=tags, + tagfile=tagfile, hosts=hosts, + hostfile=hostfile) + invTag.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts + hostsf) + failed = False + for host, taglist in invList.entries(): + if host not in hosts + hostsf: + failed = True + break + for x in tagsf + tags: + if x not in taglist: + failed = True + break + if failed: + break + os.remove(tagfile) + os.remove(hostfile) + if failed: + self.fail() + + def test_inventory_tag_del_for_all_hosts(self): + tags = ("all",) + tagsf = ("charge",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tagsf: + f.write("{}\n".format(x)) + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="del", tags=tags, + tagfile=tagfile) + invTag.run() + invList = inventory.InventoryList(db_basedir=inventory_dir) + failed = False + for host, taglist in invList.entries(): + for x in tagsf + tags: + if x in taglist: + failed = True + break + if failed: + break + os.remove(tagfile) + if failed: + self.fail() + + def test_inventory_tag_del(self): + tags = ("europe", "africa",) + tagsf = ("free", ) + hosts = ("loadbalancer1", "loadbalancer2", "shell2",) + hostsf = ("web2", "web3",) + tagfile = op.join(fixtures, "tags") + with open(tagfile, "w") as f: + for x in tagsf: + f.write("{}\n".format(x)) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostsf: + f.write("{}\n".format(x)) + invTag = inventory.InventoryTag(db_basedir=inventory_dir, + action="del", tags=tags, + tagfile=tagfile, hosts=hosts, + hostfile=hostfile) + invTag.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts + hostsf) + failed = False + for host, taglist in invList.entries(): + if host not in hosts + hostsf: + failed = True + break + for x in tagsf + tags: + if x in taglist: + failed = True + break + if failed: + break + os.remove(tagfile) + os.remove(hostfile) + if failed: + self.fail() + + def test_inventory_tag_del_all_tags(self): + hosts = ("web3", "shell1",) + hostsf = ("shell2", "loadbalancer1",) + hostfile = op.join(fixtures, "hosts") + with open(hostfile, "w") as f: + for x in hostsf: + f.write("{}\n".format(x)) + invHost = inventory.InventoryHost(db_basedir=inventory_dir, + action="del", all=True, + hosts=hosts, hostfile=hostfile) + invHost.run() + invList = inventory.InventoryList(db_basedir=inventory_dir, + hosts=hosts + hostsf) + for host, htags in invList.entries(): + self.assertEqual(htags, ()) + os.remove(hostfile) + +if __name__ == "__main__": + unittest.main() diff --git a/cdist/test/manifest/__init__.py b/cdist/test/manifest/__init__.py index 3e07c1a7..e0da2d9f 100644 --- a/cdist/test/manifest/__init__.py +++ b/cdist/test/manifest/__init__.py @@ -53,6 +53,7 @@ class ManifestTestCase(test.CdistTestCase): base_root_path = os.path.join(out_path, hostdir) self.local = local.Local( target_host=self.target_host, + target_host_tags=self.target_host_tags, base_root_path=base_root_path, host_dir_name=hostdir, exec_path=cdist.test.cdist_exec_path, @@ -93,6 +94,8 @@ class ManifestTestCase(test.CdistTestCase): self.local.type_path) self.assertEqual(output_dict['__manifest'], self.local.manifest_path) self.assertEqual(output_dict['__files'], self.local.files_path) + self.assertEqual(output_dict['__target_host_tags'], + self.local.target_host_tags) def test_type_manifest_environment(self): cdist_type = core.CdistType(self.local.type_path, '__dump_environment') @@ -126,6 +129,8 @@ class ManifestTestCase(test.CdistTestCase): self.assertEqual(output_dict['__object_id'], cdist_object.object_id) self.assertEqual(output_dict['__object_name'], cdist_object.name) self.assertEqual(output_dict['__files'], self.local.files_path) + self.assertEqual(output_dict['__target_host_tags'], + self.local.target_host_tags) def test_debug_env_setup(self): current_level = self.log.getEffectiveLevel() diff --git a/cdist/test/manifest/fixtures/conf/manifest/dump_environment b/cdist/test/manifest/fixtures/conf/manifest/dump_environment index 702145e2..9f9df372 100755 --- a/cdist/test/manifest/fixtures/conf/manifest/dump_environment +++ b/cdist/test/manifest/fixtures/conf/manifest/dump_environment @@ -9,4 +9,5 @@ __global: $__global __cdist_type_base_path: $__cdist_type_base_path __manifest: $__manifest __files: $__files +__target_host_tags: $__target_host_tags DONE diff --git a/cdist/test/manifest/fixtures/conf/type/__dump_environment/manifest b/cdist/test/manifest/fixtures/conf/type/__dump_environment/manifest index 757d07b5..fec5cb3f 100755 --- a/cdist/test/manifest/fixtures/conf/type/__dump_environment/manifest +++ b/cdist/test/manifest/fixtures/conf/type/__dump_environment/manifest @@ -13,4 +13,5 @@ __object: $__object __object_id: $__object_id __object_name: $__object_name __files: $__files +__target_host_tags: $__target_host_tags DONE diff --git a/completions/bash/cdist-completion.bash b/completions/bash/cdist-completion.bash index 6b58e2a2..1311384a 100644 --- a/completions/bash/cdist-completion.bash +++ b/completions/bash/cdist-completion.bash @@ -5,8 +5,8 @@ _cdist() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" prevprev="${COMP_WORDS[COMP_CWORD-2]}" - opts="-h --help -d --debug -v --verbose -V --version" - cmds="banner shell config install" + opts="-h --help -q --quiet -v --verbose -V --version" + cmds="banner config install inventory shell" case "${prevprev}" in shell) @@ -18,6 +18,41 @@ _cdist() ;; esac ;; + inventory) + case "${prev}" in + list) + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --invento/y -a --all -f --file -H --host-only \ + -t --tag" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + add-host) + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --inventory -f --file" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + del-host) + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --inventory -a --all -f --file" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + add-tag) + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --inventory -f --file -T --tag-file -t --taglist" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + del-tag) + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --inventory -a --all -f --file -T --tag-file -t --taglist" + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + esac + ;; esac case "${prev}" in @@ -26,23 +61,31 @@ _cdist() return 0 ;; banner) - opts="-h --help -d --debug -v --verbose" + opts="-h --help -q --quiet -v --verbose" COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 ;; shell) - opts="-h --help -d --debug -v --verbose -s --shell" + opts="-h --help -q --quiet -v --verbose -s --shell" COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 ;; config|install) - opts="-h --help -d --debug -v --verbose -b --beta \ - -C --cache-path-pattern -c --conf-dir -f --file -i --initial-manifest -j --jobs \ - -n --dry-run -o --out-dir -p --parallel -r --remote-out-dir -s --sequential \ - --remote-copy --remote-exec" + opts="-h --help -q --quiet -v --verbose -b --beta \ + -I --inventory -C --cache-path-pattern -c --conf-dir \ + -f --file -i --initial-manifest -A --all-tagged \ + -j --jobs -n --dry-run -o --out-dir -p --parallel \ + -r --remote-out-dir \ + -s --sequential --remote-copy --remote-exec -t --tag -a --all" COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 ;; + inventory) + cmds="list add-host del-host add-tag del-tag" + opts="-h --help -q --quiet -v --verbose" + COMPREPLY=( $(compgen -W "${opts} ${cmds}" -- ${cur}) ) + return 0 + ;; *) ;; esac diff --git a/completions/zsh/_cdist b/completions/zsh/_cdist index 082cad12..405023ed 100644 --- a/completions/zsh/_cdist +++ b/completions/zsh/_cdist @@ -11,16 +11,16 @@ _cdist() case $state in opts_cmds) - _arguments '1:Options and commands:(banner config shell install -h --help -d --debug -v --verbose -V --version)' + _arguments '1:Options and commands:(banner config install inventory shell -h --help -q --quiet -v --verbose -V --version)' ;; *) case $words[2] in -*) - opts=(-h --help -d --debug -v --verbose -V --version) + opts=(-h --help -q --quiet -v --verbose -V --version) compadd "$@" -- $opts ;; banner) - opts=(-h --help -d --debug -v --verbose) + opts=(-h --help -q --quiet -v --verbose) compadd "$@" -- $opts ;; shell) @@ -30,16 +30,45 @@ _cdist() compadd "$@" -- $shells ;; *) - opts=(-h --help -d --debug -v --verbose -s --shell) + opts=(-h --help -q --quiet -v --verbose -s --shell) compadd "$@" -- $opts ;; esac ;; config|install) - opts=(-h --help -d --debug -v --verbose -b --beta -C --cache-path-pattern -c --conf-dir -f --file -i --initial-manifest -j --jobs -n --dry-run -o --out-dir -p --parallel -r --remote-out-dir -s --sequential --remote-copy --remote-exec) + opts=(-h --help -q --quiet -v --verbose -a --all -b --beta -C --cache-path-pattern -c --conf-dir -f --file -i --initial-manifest -j --jobs -n --dry-run -o --out-dir -p --parallel -r --remote-out-dir -s --sequential --remote-copy --remote-exec -t --tag -I --inventory -A --all-tagged) compadd "$@" -- $opts ;; - *) + inventory) + case $words[3] in + list) + opts=(-h --help -q --quiet -v --verbose -b --beta -I --inventory -a --all -f --file -H --host-only -t --tag) + compadd "$@" -- $opts + ;; + add-host) + opts=(-h --help -q --quiet -v --verbose -b --beta -I --inventory -f --file) + compadd "$@" -- $opts + ;; + del-host) + opts=(-h --help -q --quiet -v --verbose -b --beta -I --inventory -a --all -f --file) + compadd "$@" -- $opts + ;; + add-tag) + opts=(-h --help -q --quiet -v --verbose -b --beta -I --inventory -f --file -T --tag-file -t --taglist) + compadd "$@" -- $opts + ;; + del-tag) + opts=(-h --help -q --quiet -v --verbose -b --beta -I --inventory -a --all -f --file -T --tag-file -t --taglist) + compadd "$@" -- $opts + ;; + *) + cmds=(list add-host del-host add-tag del-tag) + opts=(-h --help -q --quiet -v --verbose) + compadd "$@" -- $cmds $opts + ;; + esac + ;; + *) ;; esac esac diff --git a/docs/changelog b/docs/changelog index fee1e1ff..b8c58adf 100644 --- a/docs/changelog +++ b/docs/changelog @@ -2,6 +2,10 @@ Changelog --------- next: + * Core: Add inventory functionality (Darko Poljak) + * Core: Expose inventory host tags in __target_host_tags env var (Darko Poljak) + +4.5.0: 2017-07-20 * Types: Fix install types (Steven Armstrong) * Core: Add -r command line option for setting remote base path (Steven Armstrong) * Core: Allow manifest and gencode scripts to be written in any language (Darko Poljak) diff --git a/docs/src/cdist-inventory.rst b/docs/src/cdist-inventory.rst new file mode 100644 index 00000000..584fe310 --- /dev/null +++ b/docs/src/cdist-inventory.rst @@ -0,0 +1,211 @@ +Inventory +========= + +Introduction +------------ + +cdist comes with simple built-in tag based inventory. It is a simple inventory +with list of hosts and a host has a list of tags. +Inventory functionality is still in **beta** so it can be used only if beta +command line flag is specified (-b, --beta) or setting CDIST_BETA env var. + +Description +----------- + +The idea is to have simple tagging inventory. There is a list of hosts and for +each host there are tags. Inventory database is a set of files under inventory +database base directory. Filename equals hostname. Each file contains tags for +hostname with each tag on its own line. + +Using inventory you can now configure hosts by selecting them by tags. + +Tags have no values, as tags are just tags. Tag name-value would in this +context mean that host has two tags and it is selected by specifying that both +tags are present. + +This inventory is **KISS** cdist built-in inventory database. You can maintain it +using cdist inventory interface or using standard UNIX tools. + +cdist inventory interface +------------------------- + +With cdist inventory interface you can list host(s) and tag(s), add host(s), +add tag(s), delete host(s) and delete tag(s). + +Configuring hosts using inventory +--------------------------------- + +config command now has new options, **-t**, **-a** and **-A**. + +**-A** means that all hosts in tag db is selected. + +**-a** means that selected hosts must contain ALL specified tags. + +**-t** means that host specifies tag - all hosts that have specified tags are +selected. + +Examples +-------- + +.. code-block:: sh + + # List inventory content + $ cdist inventory list -b + + # List inventory for specified host localhost + $ cdist inventory list -b localhost + + # List inventory for specified tag loadbalancer + $ cdist inventory list -b -t loadbalancer + + # Add hosts to inventory + $ cdist inventory add-host -b web1 web2 web3 + + # Delete hosts from file old-hosts from inventory + $ cdist inventory del-host -b -f old-hosts + + # Add tags to specifed hosts + $ cdist inventory add-tag -b -t europe,croatia,web,static web1 web2 + + # Add tag to all hosts in inventory + $ cdist inventory add-tag -b -t vm + + # Delete all tags from specified host + $ cdist inventory del-tag -b -a localhost + + # Delete tags read from stdin from hosts specified by file hosts + $ cdist inventory del-tag -b -T - -f hosts + + # Configure hosts from inventory with any of specified tags + $ cdist config -b -t web dynamic + + # Configure hosts from inventory with all specified tags + $ cdist config -b -t -a web dynamic + + # Configure all hosts from inventory db + $ cdist config -b -A + +Example of manipulating database +-------------------------------- + +.. code-block:: sh + + $ python3 scripts/cdist inventory list -b + $ python3 scripts/cdist inventory add-host -b localhost + $ python3 scripts/cdist inventory add-host -b test.mycloud.net + $ python3 scripts/cdist inventory list -b + localhost + test.mycloud.net + $ python3 scripts/cdist inventory add-host -b web1.mycloud.net web2.mycloud.net shell1.mycloud.net shell2.mycloud.net + $ python3 scripts/cdist inventory list -b + localhost + test.mycloud.net + web1.mycloud.net + web2.mycloud.net + shell1.mycloud.net + shell2.mycloud.net + $ python3 scripts/cdist inventory add-tag -b -t web web1.mycloud.net web2.mycloud.net + $ python3 scripts/cdist inventory add-tag -b -t shell shell1.mycloud.net shell2.mycloud.net + $ python3 scripts/cdist inventory add-tag -b -t cloud + $ python3 scripts/cdist inventory list -b + localhost cloud + test.mycloud.net cloud + web1.mycloud.net cloud,web + web2.mycloud.net cloud,web + shell1.mycloud.net cloud,shell + shell2.mycloud.net cloud,shell + $ python3 scripts/cdist inventory add-tag -b -t test,web,shell test.mycloud.net + $ python3 scripts/cdist inventory list -b + localhost cloud + test.mycloud.net cloud,shell,test,web + web1.mycloud.net cloud,web + web2.mycloud.net cloud,web + shell1.mycloud.net cloud,shell + shell2.mycloud.net cloud,shell + $ python3 scripts/cdist inventory del-tag -b -t shell test.mycloud.net + $ python3 scripts/cdist inventory list -b + localhost cloud + test.mycloud.net cloud,test,web + web1.mycloud.net cloud,web + web2.mycloud.net cloud,web + shell1.mycloud.net cloud,shell + shell2.mycloud.net cloud,shell + $ python3 scripts/cdist inventory add-tag -b -t all + $ python3 scripts/cdist inventory add-tag -b -t mistake + $ python3 scripts/cdist inventory list -b + localhost all,cloud,mistake + test.mycloud.net all,cloud,mistake,test,web + web1.mycloud.net all,cloud,mistake,web + web2.mycloud.net all,cloud,mistake,web + shell1.mycloud.net all,cloud,mistake,shell + shell2.mycloud.net all,cloud,mistake,shell + $ python3 scripts/cdist inventory del-tag -b -t mistake + $ python3 scripts/cdist inventory list -b + localhost all,cloud + test.mycloud.net all,cloud,test,web + web1.mycloud.net all,cloud,web + web2.mycloud.net all,cloud,web + shell1.mycloud.net all,cloud,shell + shell2.mycloud.net all,cloud,shell + $ python3 scripts/cdist inventory del-host -b localhost + $ python3 scripts/cdist inventory list -b + test.mycloud.net all,cloud,test,web + web1.mycloud.net all,cloud,web + web2.mycloud.net all,cloud,web + shell1.mycloud.net all,cloud,shell + shell2.mycloud.net all,cloud,shell + $ python3 scripts/cdist inventory list -b -t web + test.mycloud.net all,cloud,test,web + web1.mycloud.net all,cloud,web + web2.mycloud.net all,cloud,web + $ python3 scripts/cdist inventory list -b -t -a web test + test.mycloud.net all,cloud,test,web + $ python3 scripts/cdist inventory list -b -t -a web all + test.mycloud.net all,cloud,test,web + web1.mycloud.net all,cloud,web + web2.mycloud.net all,cloud,web + $ python3 scripts/cdist inventory list -b -t web all + test.mycloud.net all,cloud,test,web + web1.mycloud.net all,cloud,web + web2.mycloud.net all,cloud,web + shell1.mycloud.net all,cloud,shell + shell2.mycloud.net all,cloud,shell + $ cd cdist/inventory + $ ls -1 + shell1.mycloud.net + shell2.mycloud.net + test.mycloud.net + web1.mycloud.net + web2.mycloud.net + $ ls -l + total 20 + -rw-r--r-- 1 darko darko 16 Jun 24 12:43 shell1.mycloud.net + -rw-r--r-- 1 darko darko 16 Jun 24 12:43 shell2.mycloud.net + -rw-r--r-- 1 darko darko 19 Jun 24 12:43 test.mycloud.net + -rw-r--r-- 1 darko darko 14 Jun 24 12:43 web1.mycloud.net + -rw-r--r-- 1 darko darko 14 Jun 24 12:43 web2.mycloud.net + $ cat test.mycloud.net + test + all + web + cloud + $ cat web2.mycloud.net + all + web + cloud + +For more info about inventory commands and options see `cdist `_\ (1). + +Using external inventory +------------------------ + +cdist can be used with any external inventory where external inventory is +some storage or database from which you can get a list of hosts to configure. +cdist can then be fed with this list of hosts through stdin or file using +**-f** option. For example, if your host list is stored in sqlite3 database +hosts.db and you want to select hosts which purpose is **django** then you +can use it with cdist like: + +.. code-block:: sh + + $ sqlite3 hosts.db "select hostname from hosts where purpose = 'django';" | cdist config diff --git a/docs/src/cdist-reference.rst.sh b/docs/src/cdist-reference.rst.sh index 5889ded9..89820358 100755 --- a/docs/src/cdist-reference.rst.sh +++ b/docs/src/cdist-reference.rst.sh @@ -63,6 +63,10 @@ cdist/conf/ The distribution configuration directory. This contains types and explorers to be used. +cdist/inventory/ + The distribution inventory directory. + This path is relative to cdist installation directory. + confdir Cdist will use all available configuration directories and create a temporary confdir containing links to the real configuration directories. @@ -239,6 +243,9 @@ __target_fqdn This variable is derived from **__target_host** (using **socket.getfqdn()**). Available for: explorer, initial manifest, type explorer, type manifest, type gencode, shell. +__target_host_tags + Comma separated list of target host tags. + Available for: explorer, initial manifest, type explorer, type manifest, type gencode, shell. __type Path to the current type. Available for: type manifest, type gencode. @@ -274,6 +281,9 @@ CDIST_REMOTE_EXEC CDIST_REMOTE_COPY Use this command for remote copy (should behave like scp). +CDIST_INVENTORY_DIR + Use this directory as inventory directory. + CDIST_BETA Enable beta functionalities. diff --git a/docs/src/index.rst b/docs/src/index.rst index b33b707d..42c21199 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -24,6 +24,7 @@ Contents: cdist-explorer cdist-messaging cdist-parallelization + cdist-inventory cdist-reference cdist-best-practice cdist-stages diff --git a/docs/src/man1/cdist.rst b/docs/src/man1/cdist.rst index 37ea4969..d6bd1c8f 100644 --- a/docs/src/man1/cdist.rst +++ b/docs/src/man1/cdist.rst @@ -11,21 +11,46 @@ SYNOPSIS :: - cdist [-h] [-q] [-v] [-V] {banner,config,shell,install} ... + cdist [-h] [-q] [-v] [-V] {banner,config,install,inventory,shell} ... cdist banner [-h] [-q] [-v] cdist config [-h] [-q] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] [-i MANIFEST] [-j [JOBS]] [-n] [-o OUT_PATH] - [--remote-copy REMOTE_COPY] [--remote-exec REMOTE_EXEC] - [-f HOSTFILE] [-p] [-r REMOTE_OUT_PATH] [-s] - [host [host ...]] + [-r REMOTE_OUT_DIR] [--remote-copy REMOTE_COPY] + [--remote-exec REMOTE_EXEC] [-I INVENTORY_DIR] [-A] [-a] + [-f HOSTFILE] [-p] [-s] [-t] + [host [host ...]] cdist install [-h] [-q] [-v] [-b] [-C CACHE_PATH_PATTERN] [-c CONF_DIR] [-i MANIFEST] [-j [JOBS]] [-n] [-o OUT_PATH] - [--remote-copy REMOTE_COPY] [--remote-exec REMOTE_EXEC] - [-f HOSTFILE] [-p] [-r REMOTE_OUT_PATH] [-s] - [host [host ...]] + [-r REMOTE_OUT_DIR] [--remote-copy REMOTE_COPY] + [--remote-exec REMOTE_EXEC] [-I INVENTORY_DIR] [-A] [-a] + [-f HOSTFILE] [-p] [-s] [-t] + [host [host ...]] + + cdist inventory [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] + {add-host,add-tag,del-host,del-tag,list} ... + + cdist inventory add-host [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] + [-f HOSTFILE] + [host [host ...]] + + cdist inventory add-tag [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] + [-f HOSTFILE] [-T TAGFILE] [-t TAGLIST] + [host [host ...]] + + cdist inventory del-host [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] [-a] + [-f HOSTFILE] + [host [host ...]] + + cdist inventory del-tag [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] [-a] + [-f HOSTFILE] [-T TAGFILE] [-t TAGLIST] + [host [host ...]] + + cdist inventory list [-h] [-q] [-v] [-b] [-I INVENTORY_DIR] [-a] + [-f HOSTFILE] [-H] [-t] + [host [host ...]] cdist shell [-h] [-q] [-v] [-s SHELL] @@ -72,6 +97,15 @@ CONFIG/INSTALL -------------- Configure/install one or more hosts. +.. option:: -A, --all-tagged + + use all hosts present in tags db + +.. option:: -a, --all + + list hosts that have all specified tags, if -t/--tag + is specified + .. option:: -b, --beta Enable beta functionality. @@ -103,6 +137,16 @@ Configure/install one or more hosts. read hosts from stdin. For the file format see :strong:`HOSTFILE FORMAT` below. +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdit/inventory is + used, otherwise distribution inventory directory is + used. + .. option:: -i MANIFEST, --initial-manifest MANIFEST Path to a cdist manifest or - to read from stdin @@ -141,6 +185,10 @@ Configure/install one or more hosts. Command to use for remote execution (should behave like ssh) +.. option:: -t, --tag + + host is specified by tag, not hostname/address; list + all hosts that contain any of specified tags HOSTFILE FORMAT ~~~~~~~~~~~~~~~ @@ -174,6 +222,246 @@ Resulting path is used to specify cache path subdirectory under which current host cache data are saved. +INVENTORY +--------- +Manage inventory database. +Currently in beta with all sub-commands. + + +INVENTORY ADD-HOST +------------------ +Add host(s) to inventory database. + +.. option:: host + + host(s) to add + +.. option:: -b, --beta + + Enable beta functionalities. Beta functionalities + include inventory command with all sub-commands and + all options; config sub-command options: -j/--jobs, + -t/--tag, -a/--all. + + Can also be enabled using CDIST_BETA env var. + +.. option:: -f HOSTFILE, --file HOSTFILE + + Read additional hosts to add from specified file or + from stdin if '-' (each host on separate line). If no + host or host file is specified then, by default, read + from stdin. Hostfile format is the same as config hostfile format. + +.. option:: -h, --help + + show this help message and exit + +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdist/inventory is + used, otherwise distribution inventory directory is + used. + + +INVENTORY ADD-TAG +----------------- +Add tag(s) to inventory database. + +.. option:: host + + list of host(s) for which tags are added + +.. option:: -b, --beta + + Enable beta functionalities. Beta functionalities + include inventory command with all sub-commands and + all options; config sub-command options: -j/--jobs, + -t/--tag, -a/--all. + + Can also be enabled using CDIST_BETA env var. + +.. option:: -f HOSTFILE, --file HOSTFILE + + Read additional hosts to add tags from specified file + or from stdin if '-' (each host on separate line). If + no host or host file is specified then, by default, + read from stdin. If no tags/tagfile nor hosts/hostfile + are specified then tags are read from stdin and are + added to all hosts. Hostfile format is the same as config hostfile format. + +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdist/inventory is + used, otherwise distribution inventory directory is + used. + +.. option:: -T TAGFILE, --tag-file TAGFILE + + Read additional tags to add from specified file or + from stdin if '-' (each tag on separate line). If no + tag or tag file is specified then, by default, read + from stdin. If no tags/tagfile nor hosts/hostfile are + specified then tags are read from stdin and are added + to all hosts. Tagfile format is the same as config hostfile format. + +.. option:: -t TAGLIST, --taglist TAGLIST + + Tag list to be added for specified host(s), comma + separated values + + +INVENTORY DEL-HOST +------------------ +Delete host(s) from inventory database. + +.. option:: host + + host(s) to delete + +.. option:: -a, --all + + Delete all hosts + +.. option:: -b, --beta + + Enable beta functionalities. Beta functionalities + include inventory command with all sub-commands and + all options; config sub-command options: -j/--jobs, + -t/--tag, -a/--all. + + Can also be enabled using CDIST_BETA env var. + +.. option:: -f HOSTFILE, --file HOSTFILE + + Read additional hosts to delete from specified file or + from stdin if '-' (each host on separate line). If no + host or host file is specified then, by default, read + from stdin. Hostfile format is the same as config hostfile format. + +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdist/inventory is + used, otherwise distribution inventory directory is + used. + + +INVENTORY DEL-TAG +----------------- +Delete tag(s) from inventory database. + +.. option:: host + + list of host(s) for which tags are deleted + +.. option:: -a, --all + + Delete all tags for specified host(s) + +.. option:: -b, --beta + + Enable beta functionalities. Beta functionalities + include inventory command with all sub-commands and + all options; config sub-command options: -j/--jobs, + -t/--tag, -a/--all. + + Can also be enabled using CDIST_BETA env var. + +.. option:: -f HOSTFILE, --file HOSTFILE + + Read additional hosts to delete tags for from + specified file or from stdin if '-' (each host on + separate line). If no host or host file is specified + then, by default, read from stdin. If no tags/tagfile + nor hosts/hostfile are specified then tags are read + from stdin and are deleted from all hosts. Hostfile + format is the same as config hostfile format. + +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdist/inventory is + used, otherwise distribution inventory directory is + used. + +.. option:: -T TAGFILE, --tag-file TAGFILE + + Read additional tags from specified file or from stdin + if '-' (each tag on separate line). If no tag or tag + file is specified then, by default, read from stdin. + If no tags/tagfile nor hosts/hostfile are specified + then tags are read from stdin and are added to all + hosts. Tagfile format is the same as config hostfile format. + +.. option:: -t TAGLIST, --taglist TAGLIST + + Tag list to be deleted for specified host(s), comma + separated values + + +INVENTORY LIST +-------------- +List inventory database. + +.. option:: host + + host(s) to list + +.. option:: -a, --all + + list hosts that have all specified tags, if -t/--tag + is specified + +.. option:: -b, --beta + + Enable beta functionalities. Beta functionalities + include inventory command with all sub-commands and + all options; config sub-command options: -j/--jobs, + -t/--tag, -a/--all. + + Can also be enabled using CDIST_BETA env var. + +.. option:: -f HOSTFILE, --file HOSTFILE + + Read additional hosts to list from specified file or + from stdin if '-' (each host on separate line). If no + host or host file is specified then, by default, list + all. Hostfile format is the same as config hostfile format. + +.. option:: -H, --host-only + + Suppress tags listing + +.. option:: -I INVENTORY_DIR, --inventory INVENTORY_DIR + + Use specified custom inventory directory. Inventory + directory is set up by the following rules: if this + argument is set then specified directory is used, if + CDIST_INVENTORY_DIR env var is set then its value is + used, if HOME env var is set then ~/.cdist/inventory is + used, otherwise distribution inventory directory is + used. + +.. option:: -t, --tag + + host is specified by tag, not hostname/address; list + all hosts that contain any of specified tags + + SHELL ----- This command allows you to spawn a shell that enables access @@ -186,14 +474,21 @@ usage. Its primary use is for debugging type parameters. Select shell to use, defaults to current shell. Used shell should be POSIX compatible shell. + FILES ----- ~/.cdist Your personal cdist config directory. If exists it will be automatically used. +~/.cdist/inventory + The home inventory directory. If ~/.cdist exists it will be used as + default inventory directory. cdist/conf The distribution configuration directory. It contains official types and explorers. This path is relative to cdist installation directory. +cdist/inventory + The distribution inventory directory. + This path is relative to cdist installation directory. NOTES ----- @@ -243,6 +538,43 @@ EXAMPLES # Install ikq05.ethz.ch with debug enabled % cdist install -vvv ikq05.ethz.ch + # List inventory content + % cdist inventory list -b + + # List inventory for specified host localhost + % cdist inventory list -b localhost + + # List inventory for specified tag loadbalancer + % cdist inventory list -b -t loadbalancer + + # Add hosts to inventory + % cdist inventory add-host -b web1 web2 web3 + + # Delete hosts from file old-hosts from inventory + % cdist inventory del-host -b -f old-hosts + + # Add tags to specifed hosts + % cdist inventory add-tag -b -t europe,croatia,web,static web1 web2 + + # Add tag to all hosts in inventory + % cdist inventory add-tag -b -t vm + + # Delete all tags from specified host + % cdist inventory del-tag -b -a localhost + + # Delete tags read from stdin from hosts specified by file hosts + % cdist inventory del-tag -b -T - -f hosts + + # Configure hosts from inventory with any of specified tags + % cdist config -b -t web dynamic + + # Configure hosts from inventory with all specified tags + % cdist config -b -t -a web dynamic + + # Configure all hosts from inventory db + $ cdist config -b -A + + ENVIRONMENT ----------- TMPDIR, TEMP, TMP @@ -272,6 +604,9 @@ CDIST_REMOTE_EXEC CDIST_REMOTE_COPY Use this command for remote copy (should behave like scp). +CDIST_INVENTORY_DIR + Use this directory as inventory directory. + CDIST_BETA Enable beta functionality. diff --git a/scripts/cdist b/scripts/cdist index 81220ca3..d9dce35f 100755 --- a/scripts/cdist +++ b/scripts/cdist @@ -33,6 +33,7 @@ def commandline(): import cdist.config import cdist.install import cdist.shell + import cdist.inventory import shutil import os From c706b9eefba010e518dc1fdfbb1597545894a5a0 Mon Sep 17 00:00:00 2001 From: Ander Punnar Date: Fri, 21 Jul 2017 10:19:02 +0300 Subject: [PATCH 07/10] check current timezone before doing anything --- .../conf/type/__timezone/explorer/timezone_is | 20 +++++++++++++++++++ cdist/conf/type/__timezone/gencode-remote | 9 +++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100755 cdist/conf/type/__timezone/explorer/timezone_is diff --git a/cdist/conf/type/__timezone/explorer/timezone_is b/cdist/conf/type/__timezone/explorer/timezone_is new file mode 100755 index 00000000..7e9de076 --- /dev/null +++ b/cdist/conf/type/__timezone/explorer/timezone_is @@ -0,0 +1,20 @@ +#!/bin/sh -e +# +# 2016 Ander Punnar (cdist at kvlt.ee) +# +# 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 . +# +cat /etc/timezone diff --git a/cdist/conf/type/__timezone/gencode-remote b/cdist/conf/type/__timezone/gencode-remote index d72da918..e512f861 100755 --- a/cdist/conf/type/__timezone/gencode-remote +++ b/cdist/conf/type/__timezone/gencode-remote @@ -20,11 +20,16 @@ # # This type allows to configure the desired localtime timezone. -timezone="$__object_id" +timezone_is=$(cat "$__object/explorer/timezone_is") +timezone_should="$__object_id" os=$(cat "$__global/explorer/os") +if [ "$timezone_is" = "$timezone_should" ]; then + exit 0 +fi + case "$os" in ubuntu|debian|devuan) - echo "echo \"$timezone\" > /etc/timezone" + echo "echo \"$timezone_should\" > /etc/timezone" ;; esac From 2c56622eebca26002f882bb2d17f20ccd27bc753 Mon Sep 17 00:00:00 2001 From: Ander Punnar Date: Fri, 21 Jul 2017 10:22:54 +0300 Subject: [PATCH 08/10] check file first --- cdist/conf/type/__timezone/explorer/timezone_is | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cdist/conf/type/__timezone/explorer/timezone_is b/cdist/conf/type/__timezone/explorer/timezone_is index 7e9de076..ec957139 100755 --- a/cdist/conf/type/__timezone/explorer/timezone_is +++ b/cdist/conf/type/__timezone/explorer/timezone_is @@ -17,4 +17,5 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # -cat /etc/timezone + +[ -f /etc/timezone ] && cat /etc/timezone From 6bfe02094d960587d181d77437936189666d0248 Mon Sep 17 00:00:00 2001 From: Ander Punnar Date: Fri, 21 Jul 2017 10:23:05 +0300 Subject: [PATCH 09/10] year is 2017 --- cdist/conf/type/__timezone/explorer/timezone_is | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdist/conf/type/__timezone/explorer/timezone_is b/cdist/conf/type/__timezone/explorer/timezone_is index ec957139..4e918121 100755 --- a/cdist/conf/type/__timezone/explorer/timezone_is +++ b/cdist/conf/type/__timezone/explorer/timezone_is @@ -1,6 +1,6 @@ #!/bin/sh -e # -# 2016 Ander Punnar (cdist at kvlt.ee) +# 2017 Ander Punnar (cdist at kvlt.ee) # # This file is part of cdist. # From 493150650c4c4751bc46cf459542ccc4c1339e07 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Fri, 21 Jul 2017 17:21:08 +0200 Subject: [PATCH 10/10] Update changelog --- docs/changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog b/docs/changelog index b8c58adf..f4bfaffb 100644 --- a/docs/changelog +++ b/docs/changelog @@ -4,6 +4,7 @@ Changelog next: * Core: Add inventory functionality (Darko Poljak) * Core: Expose inventory host tags in __target_host_tags env var (Darko Poljak) + * Type __timezone: Check current timezone before doing anything (Ander Punnar) 4.5.0: 2017-07-20 * Types: Fix install types (Steven Armstrong)