cdist/bin/cdist

229 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2010-2011 Nico Schottelius (nico-cdist at schottelius.org)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
#
import argparse
import logging
import os
import subprocess
import shutil
import sys
import tempfile
# Given paths from installation
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
CONF_DIR = os.path.join(BASE_DIR, "conf")
GLOBAL_EXPLORER_DIR = os.path.join(CONF_DIR, "explorer")
LIB_DIR = os.path.join(BASE_DIR, "lib")
MANIFEST_DIR = os.path.join(CONF_DIR, "manifest")
REMOTE_BASE_DIR = "/var/lib/cdist"
REMOTE_CONF_DIR = os.path.join(REMOTE_BASE_DIR, "conf")
REMOTE_GLOBAL_EXPLORER_DIR = os.path.join(REMOTE_CONF_DIR, "explorer")
VERSION = "2.0.0"
#class Context(object):
#
# def __init__(self, target_host):
# self.target_host = target_host
#
# # class variable
# user_selber_shuld_wenn_aendert = 'bla'
#
# # read only, aber statisch
# @property
# def remote_base_directory(self):
# return "/var/lib/cdist"
# @property.setter
#
# @property
# def special_foo(self):
# return 'foo/{0}'.format(self.target_host)
#
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
log = logging.getLogger()
class Cdist:
"""Cdist main class to hold arbitrary data"""
def __init__(self, target_host, initial_manifest=False):
self.target_host = target_host
# log.info("foobar")
# Setup directory paths
self.temp_dir = tempfile.mkdtemp()
self.out_dir = os.path.join(self.temp_dir, "out")
os.mkdir(self.out_dir)
self.global_explorer_out_dir = os.path.join(self.out_dir, "explorer")
os.mkdir(self.global_explorer_out_dir)
# Mostly static, but can be overwritten on user demand
if initial_manifest:
self.initial_manifest = initial_manifest
else:
self.initial_manifest = os.path.join(MANIFEST_DIR, "init")
def cleanup(self):
# Do not use in __del__:
# http://docs.python.org/reference/datamodel.html#customization
# "other globals referenced by the __del__() method may already have been deleted
# or in the process of being torn down (e.g. the import machinery shutting down)"
#
print("I should cleanup " + self.temp_dir)
# shutil.rmtree(self.temp_dir)
def exit_error(self, *args):
log.error(*args)
sys.exit(1)
def shell_run_or_debug_fail(self, script, *args, **kargs):
# Manually execute /bin/sh, because sh -e does what we want
# and sh -c -e does not exit if /bin/false called
args[0].insert(0,"/bin/sh")
args[0].insert(1,"-e")
log.debug("Shell exec: " + " ".join(*args))
try:
subprocess.check_call(*args, **kargs)
except subprocess.CalledProcessError:
script_fd = open(script)
log.error("Code that raised the error:\n" + script_fd.read())
script_fd.close()
self.exit_error("Non-Zero exit code exit of " + " ".join(*args))
def run_or_fail(self, *args, **kargs):
log.debug("Exec: " + " ".join(*args))
try:
subprocess.check_call(*args, **kargs)
except subprocess.CalledProcessError:
self.exit_error("Command failed:", " ".join(*args))
def remote_run_or_fail(self, *args, **kargs):
"""Run something on the remote side and fail is something breaks"""
newargs = ["ssh", "root@" + self.target_host]
newargs.extend(*args)
self.run_or_fail(newargs, **kargs)
def remove_remote_dir(self, destination):
self.remote_run_or_fail(["rm", "-rf", destination])
def transfer_dir(self, source, destination):
self.remove_remote_dir(destination)
self.run_or_fail(["scp", "-qr", source, "root@" + self.target_host + ":" + destination])
def global_explorer_output_path(self, explorer):
"""Returns path of the output for a global explorer"""
return os.path.join(self.global_explorer_out_dir, explorer)
def remote_global_explorer_path(self, explorer):
"""Returns path to the remote explorer"""
return os.path.join(REMOTE_GLOBAL_EXPLORER_DIR, explorer)
def list_global_explorers(self):
"""Return list of available explorers"""
return os.listdir(GLOBAL_EXPLORER_DIR)
def transfer_global_explorers(self):
self.transfer_dir(GLOBAL_EXPLORER_DIR, REMOTE_GLOBAL_EXPLORER_DIR)
def global_explore(self):
"""Run global explorers"""
explorers = self.list_global_explorers()
if(len(explorers) == 0):
self.exit_error("No explorers found in", GLOBAL_EXPLORER_DIR)
self.transfer_global_explorers()
for explorer in explorers:
output = self.global_explorer_output_path(explorer)
output_fd = open(output, mode='w')
cmd = []
cmd.append("__explorer=" + REMOTE_GLOBAL_EXPLORER_DIR)
cmd.append(self.remote_global_explorer_path(explorer))
self.remote_run_or_fail(cmd, stdout=output_fd)
output_fd.close()
def init_deploy(self):
log.info("Creating clean directory structure")
# Ensure there is no old stuff, neither local nor remote
# remote_run_or_fail(hostname, ["rm -rf", "${__cdist_remote_base_dir}"])
#
# # Create base directories
# remote_run_or_fail(hostname,["mkdir -p", "${__cdist_remote_base_dir}"])
#
# # Link configuraion source directory - consistent with remote
# run_or_fail(["ln -sf", "$__cdist_conf_dir", "$__cdist_local_base_dir/$__cdist_name_conf_dir"])
def run_initial_manifest(self):
"""Run the initial manifest"""
log.info("Running the initial manifest")
env = os.environ.copy()
env['__target_host'] = self.target_host
self.shell_run_or_debug_fail(self.initial_manifest,
[self.initial_manifest],
env=env)
def deploy_to(self):
"""Mimic the old deploy to: Deploy to one host"""
log.info("Deploying to host " + self.target_host)
self.init_deploy()
self.global_explore()
self.run_initial_manifest()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='cdist ' + VERSION)
parser.add_argument('host', nargs='+', help='one or more hosts to operate on')
parser.add_argument('-d', '--debug', help='set log level to debug',
action='store_true')
parser.add_argument('-i', '--initial-manifest',
help='path to a cdist manifest or - to read from stdin',
dest='manifest', required=False)
parser.add_argument('-p', '--parallel',
help='operate on multiple hosts in parallel',
action='store_true', dest='parallel')
parser.add_argument('-s', '--sequential',
help='operate on multiple hosts sequentially',
action='store_false', dest='parallel')
args = parser.parse_args(sys.argv[1:])
if args.debug:
logging.root.setLevel(logging.DEBUG)
try:
log.debug(args)
for host in args.host:
c = Cdist(host, initial_manifest=args.manifest)
c.deploy_to()
c.cleanup()
except KeyboardInterrupt:
sys.exit(0)