forked from ungleich-public/cdist
391 lines
14 KiB
Python
391 lines
14 KiB
Python
|
#!/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 <http://www.gnu.org/licenses/>.
|
||
|
#
|
||
|
#
|
||
|
|
||
|
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)
|