cdist-backup/cdist/exec/local.py
2016-07-10 21:43:09 +02:00

307 lines
11 KiB
Python

# -*- coding: utf-8 -*-
#
# 2011 Steven Armstrong (steven-cdist at armstrong.cc)
# 2011-2015 Nico Schottelius (nico-cdist at schottelius.org)
# 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 io
import os
import sys
import re
import subprocess
import shutil
import logging
import tempfile
import hashlib
import cdist
import cdist.message
from cdist import core
import cdist.exec.util as exec_util
CONF_SUBDIRS_LINKED = ["explorer", "files", "manifest", "type", ]
class Local(object):
"""Execute commands locally.
All interaction with the local side should be done through this class.
Directly accessing the local side from python code is a bug.
"""
def __init__(self,
target_host,
exec_path=sys.argv[0],
initial_manifest=None,
base_path=None,
add_conf_dirs=None):
self.target_host = target_host
self._init_log()
# FIXME: stopped: create base that does not require moving later
if base_path:
base_path_parent = base_path
else:
base_path_parent = tempfile.mkdtemp()
# TODO: the below atexit hook nukes any debug info we would have
# if cdist exits with error.
# import atexit
# atexit.register(lambda: shutil.rmtree(base_path_parent))
self.hostdir = self._hostdir()
self.log.debug("Calculated temp dir for target \"{}\" is "
"\"{}\"".format(self.target_host, self.hostdir))
self.base_path = os.path.join(base_path_parent, self.hostdir)
self._init_permissions()
self.mkdir(self.base_path)
# FIXME: as well
self._init_cache_dir(None)
self.exec_path = exec_path
self.custom_initial_manifest = initial_manifest
self._add_conf_dirs = add_conf_dirs
self._init_paths()
self._init_object_marker()
self._init_conf_dirs()
@property
def dist_conf_dir(self):
return os.path.abspath(os.path.join(os.path.dirname(cdist.__file__),
"conf"))
@property
def home_dir(self):
if 'HOME' in os.environ:
return os.path.join(os.environ['HOME'], ".cdist")
else:
return None
def _hostdir(self):
# Do not assume target_host is anything that can be used as a
# directory name.
# Instead use a hash, which is known to work as directory name.
return hashlib.md5(self.target_host.encode('utf-8')).hexdigest()
def _init_log(self):
self.log = logging.getLogger(self.target_host)
def _init_permissions(self):
# Setup file permissions using umask
os.umask(0o077)
def _init_paths(self):
# Depending on out_path
self.bin_path = os.path.join(self.base_path, "bin")
self.conf_path = os.path.join(self.base_path, "conf")
self.global_explorer_out_path = os.path.join(self.base_path,
"explorer")
self.object_path = os.path.join(self.base_path, "object")
self.messages_path = os.path.join(self.base_path, "messages")
self.files_path = os.path.join(self.conf_path, "files")
# Depending on conf_path
self.global_explorer_path = os.path.join(self.conf_path, "explorer")
self.manifest_path = os.path.join(self.conf_path, "manifest")
self.initial_manifest = (self.custom_initial_manifest or
os.path.join(self.manifest_path, "init"))
self.type_path = os.path.join(self.conf_path, "type")
def _init_object_marker(self):
self.object_marker_file = os.path.join(self.base_path, "object_marker")
# Does not need to be secure - just randomly different from .cdist
self.object_marker_name = tempfile.mktemp(prefix='.cdist-', dir='')
def _init_conf_dirs(self):
self.conf_dirs = []
self.conf_dirs.append(self.dist_conf_dir)
# Is the default place for user created explorer, type and manifest
if self.home_dir:
self.conf_dirs.append(self.home_dir)
# Add directories defined in the CDIST_PATH environment variable
if 'CDIST_PATH' in os.environ:
cdist_path_dirs = re.split(r'(?<!\\):', os.environ['CDIST_PATH'])
cdist_path_dirs.reverse()
self.conf_dirs.extend(cdist_path_dirs)
# Add command line supplied directories
if self._add_conf_dirs:
self.conf_dirs.extend(self._add_conf_dirs)
def _init_directories(self):
self.mkdir(self.conf_path)
self.mkdir(self.global_explorer_out_path)
self.mkdir(self.object_path)
self.mkdir(self.bin_path)
def create_files_dirs(self):
self._init_directories()
self._create_conf_path_and_link_conf_dirs()
self._create_messages()
self._link_types_for_emulator()
self._setup_object_marker_file()
def _setup_object_marker_file(self):
with open(self.object_marker_file, 'w') as fd:
fd.write("%s\n" % self.object_marker_name)
self.log.debug("Object marker %s saved in %s" % (
self.object_marker_name, self.object_marker_file))
def _init_cache_dir(self, cache_dir):
if cache_dir:
self.cache_path = cache_dir
else:
if self.home_dir:
self.cache_path = os.path.join(self.home_dir, "cache")
else:
raise cdist.Error(
"No homedir setup and no cache dir location given")
def rmdir(self, path):
"""Remove directory on the local side."""
self.log.debug("Local rmdir: %s", path)
shutil.rmtree(path)
def mkdir(self, path):
"""Create directory on the local side."""
self.log.debug("Local mkdir: %s", path)
os.makedirs(path, exist_ok=True)
def run(self, command, env=None, return_output=False, message_prefix=None):
"""Run the given command with the given environment.
Return the output as a string.
"""
self.log.debug("Local run: %s", command)
assert isinstance(command, (list, tuple)), (
"list or tuple argument expected, got: %s" % command)
if env is None:
env = os.environ.copy()
# Export __target_host for use in __remote_{copy,exec} scripts
env['__target_host'] = self.target_host
# Export for emulator
env['__cdist_object_marker'] = self.object_marker_name
if message_prefix:
message = cdist.message.Message(message_prefix, self.messages_path)
env.update(message.env)
try:
output, errout = exec_util.call_get_output(command, env=env)
self.log.debug("Local stdout: {}".format(output))
self.log.debug("Local stderr: {}".format(errout))
if return_output:
return output.decode()
except subprocess.CalledProcessError as e:
exec_util.handle_called_process_error(e, command)
except OSError as error:
raise cdist.Error(" ".join(command) + ": " + error.args[1])
finally:
if message_prefix:
message.merge_messages()
def run_script(self, script, env=None, return_output=False,
message_prefix=None):
"""Run the given script with the given environment.
Return the output as a string.
"""
command = [os.environ.get('CDIST_LOCAL_SHELL', "/bin/sh"), "-e"]
command.append(script)
return self.run(command=command, env=env, return_output=return_output,
message_prefix=message_prefix)
def save_cache(self):
destination = os.path.join(self.cache_path, self.hostdir)
self.log.debug("Saving " + self.base_path + " to " + destination)
try:
if os.path.exists(destination):
shutil.rmtree(destination)
except PermissionError as e:
raise cdist.Error(
"Cannot delete old cache %s: %s" % (destination, e))
shutil.move(self.base_path, destination)
def _create_messages(self):
"""Create empty messages"""
with open(self.messages_path, "w"):
pass
def _create_conf_path_and_link_conf_dirs(self):
# Create destination directories
for sub_dir in CONF_SUBDIRS_LINKED:
self.mkdir(os.path.join(self.conf_path, sub_dir))
# Iterate over all directories and link the to the output dir
for conf_dir in self.conf_dirs:
self.log.debug("Checking conf_dir %s ..." % (conf_dir))
for sub_dir in CONF_SUBDIRS_LINKED:
current_dir = os.path.join(conf_dir, sub_dir)
# Allow conf dirs to contain only partial content
if not os.path.exists(current_dir):
continue
for entry in os.listdir(current_dir):
rel_entry_path = os.path.join(sub_dir, entry)
src = os.path.abspath(os.path.join(conf_dir,
sub_dir,
entry))
dst = os.path.join(self.conf_path, sub_dir, entry)
# Already exists? remove and link
if os.path.exists(dst):
os.unlink(dst)
self.log.debug("Linking %s to %s ..." % (src, dst))
try:
os.symlink(src, dst)
except OSError as e:
raise cdist.Error("Linking %s %s to %s failed: %s" % (
sub_dir, src, dst, e.__str__()))
def _link_types_for_emulator(self):
"""Link emulator to types"""
src = os.path.abspath(self.exec_path)
for cdist_type in core.CdistType.list_types(self.type_path):
dst = os.path.join(self.bin_path, cdist_type.name)
self.log.debug("Linking emulator: %s to %s", src, dst)
try:
os.symlink(src, dst)
except OSError as e:
raise cdist.Error(
"Linking emulator from %s to %s failed: %s" % (
src, dst, e.__str__()))