# -*- coding: utf-8 -*- # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) # 2011 Nico Schottelius (nico-cdist at schottelius.org) # # This file is part of cdist. # # cdist is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # cdist is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # # import io import os import sys import subprocess import logging import cdist class RemoteScriptError(cdist.Error): def __init__(self, script, command, script_content): self.script = script self.command = command self.script_content = script_content def __str__(self): plain_command = " ".join(self.command) return "Remote script execution failed: %s" % plain_command class DecodeError(cdist.Error): def __init__(self, command): self.command = command def __str__(self): return "Cannot decode output of " + " ".join(self.command) class Remote(object): """Execute commands remotely. All interaction with the remote side should be done through this class. Directly accessing the remote side from python code is a bug. """ def __init__(self, target_host, remote_base_path, remote_exec, remote_copy): self.target_host = target_host self.base_path = remote_base_path self._exec = remote_exec self._copy = remote_copy self.conf_path = os.path.join(self.base_path, "conf") self.object_path = os.path.join(self.base_path, "object") self.type_path = os.path.join(self.conf_path, "type") self.global_explorer_path = os.path.join(self.conf_path, "explorer") self.log = logging.getLogger(self.target_host) def create_directories(self): self.rmdir(self.base_path) self.mkdir(self.base_path) self.mkdir(self.conf_path) def rmdir(self, path): """Remove directory on the remote side.""" self.log.debug("Remote rmdir: %s", path) self.run(["rm", "-rf", path]) def mkdir(self, path): """Create directory on the remote side.""" self.log.debug("Remote mkdir: %s", path) self.run(["mkdir", "-p", path]) def transfer(self, source, destination): """Transfer a file or directory to the remote side.""" self.log.debug("Remote transfer: %s -> %s", source, destination) self.rmdir(destination) command = self._copy.split() command.extend(["-r", source, self.target_host + ":" + destination]) self._run_command(command) def run(self, command, env=None, return_output=False): """Run the given command with the given environment on the remote side. Return the output as a string. """ # prefix given command with remote_exec cmd = self._exec.split() cmd.append(self.target_host) cmd.extend(command) return self._run_command(cmd, env=env, return_output=return_output) def _run_command(self, command, env=None, return_output=False): """Run the given command with the given environment. Return the output as a string. """ assert isinstance(command, (list, tuple)), "list or tuple argument expected, got: %s" % command # export target_host for use in __remote_{exec,copy} scripts os_environ = os.environ.copy() os_environ['__target_host'] = self.target_host # can't pass environment to remote side, so prepend command with # variable declarations if env: cmd = ["%s=%s" % item for item in env.items()] cmd.extend(command) else: cmd = command self.log.debug("Remote run: %s", command) try: if return_output: return subprocess.check_output(command, env=os_environ).decode() else: subprocess.check_call(command, env=os_environ) except subprocess.CalledProcessError: raise cdist.Error("Command failed: " + " ".join(command)) except OSError as error: raise cdist.Error(" ".join(*args) + ": " + error.args[1]) except UnicodeDecodeError: raise DecodeError(command) def run_script(self, script, env=None, return_output=False): """Run the given script with the given environment on the remote side. Return the output as a string. """ command = self._exec.split() command.append(self.target_host) # export target_host for use in __remote_{exec,copy} scripts os_environ = os.environ.copy() os_environ['__target_host'] = self.target_host # can't pass environment to remote side, so prepend command with # variable declarations if env: command.extend(["%s=%s" % item for item in env.items()]) command.extend(["/bin/sh", "-e"]) command.append(script) self.log.debug("Remote run script: %s", command) if env: self.log.debug("Remote run script env: %s", env) try: if return_output: return subprocess.check_output(command, env=os_environ).decode() else: subprocess.check_call(command, env=os_environ) except subprocess.CalledProcessError as error: script_content = self.run(["cat", script], return_output=True) self.log.error("Code that raised the error:\n%s", script_content) raise RemoteScriptError(script, command, script_content) except EnvironmentError as error: raise cdist.Error(" ".join(command) + ": " + error.args[1])