From 79b1baa401182a796c1762ce65c95ce00e7c0818 Mon Sep 17 00:00:00 2001 From: a1fred Date: Thu, 9 Dec 2021 00:45:34 +0300 Subject: [PATCH] set run environment variables feature, use_sudo feature --- carnival/contrib/steps/apt.py | 23 ++++++++++---- carnival/contrib/steps/docker.py | 4 +-- carnival/contrib/steps/systemd.py | 12 ++++---- carnival/hosts/base/connection.py | 42 ++++++++++++++++++-------- carnival/hosts/base/host.py | 6 ++++ carnival/hosts/local/connection.py | 4 +++ carnival/hosts/local/result_promise.py | 24 ++++++++++++--- carnival/hosts/ssh/__init__.py | 4 +++ carnival/hosts/ssh/connection.py | 15 ++++++--- carnival/hosts/ssh/result_promise.py | 15 ++++++++- 10 files changed, 112 insertions(+), 37 deletions(-) diff --git a/carnival/contrib/steps/apt.py b/carnival/contrib/steps/apt.py index 606f7b3..48bb214 100644 --- a/carnival/contrib/steps/apt.py +++ b/carnival/contrib/steps/apt.py @@ -18,7 +18,11 @@ def get_validators(self) -> typing.List[validators.StepValidatorBase]: ] def run(self, c: Connection) -> None: - c.run("DEBIAN_FRONTEND=noninteractive sudo apt-get update", hide=True) + c.run( + "apt-get update", + hide=True, + env={"DEBIAN_FRONTEND": "noninteractive"}, + ) print(f"{S.BRIGHT}apt packages list{S.RESET_ALL}: {F.YELLOW}updated{F.RESET}") @@ -37,7 +41,12 @@ def get_validators(self) -> typing.List[validators.StepValidatorBase]: def run(self, c: Connection) -> typing.List[str]: versions = [] - result = c.run(f"DEBIAN_FRONTEND=noninteractive apt-cache madison {self.pkgname}", hide=True, warn=True) + result = c.run( + f"apt-cache madison {self.pkgname}", + env={"DEBIAN_FRONTEND": "noninteractive"}, + hide=True, + warn=True + ) if result.ok is False: return [] @@ -67,7 +76,8 @@ def run(self, c: Connection) -> typing.Optional[str]: :return: Версия пакета если установлен, `None` если пакет не установлен """ result = c.run( - f"DEBIAN_FRONTEND=noninteractive dpkg -l {self.pkgname} | grep '{self.pkgname}'", + f"dpkg -l {self.pkgname} | grep '{self.pkgname}'", + env={"DEBIAN_FRONTEND": "noninteractive"}, hide=True, warn=True, ) @@ -135,7 +145,7 @@ def run(self, c: Connection) -> None: if self.update: Update().run(c) - c.run(f"DEBIAN_FRONTEND=noninteractive sudo apt-get install -y {pkgname}") + c.run(f"apt-get install -y {pkgname}", env={"DEBIAN_FRONTEND": "noninteractive"}) class Install(Step): @@ -214,7 +224,7 @@ def run(self, c: Connection) -> bool: return False if self.update: - c.run("DEBIAN_FRONTEND=noninteractive sudo apt-get update", hide=True) + c.run("apt-get update", hide=True, env={"DEBIAN_FRONTEND": "noninteractive"}) for pkg in self.pkg_names: Install(pkgname=pkg, update=False).run(c=c) @@ -245,6 +255,7 @@ def run(self, c: Connection) -> None: for pkg in self.pkg_names: if IsPackageInstalled(pkg).run(c): c.run( - f"DEBIAN_FRONTEND=noninteractive sudo apt-get remove --auto-remove -y {' '.join(self.pkg_names)}", + f"apt-get remove --auto-remove -y {' '.join(self.pkg_names)}", + env={"DEBIAN_FRONTEND": "noninteractive"}, ) print(f"{S.BRIGHT}{pkg}{S.RESET_ALL}: {F.YELLOW}removed{F.RESET}") diff --git a/carnival/contrib/steps/docker.py b/carnival/contrib/steps/docker.py index 387a031..182e4dc 100644 --- a/carnival/contrib/steps/docker.py +++ b/carnival/contrib/steps/docker.py @@ -74,8 +74,8 @@ def run(self, c: Connection) -> None: return link = f"https://github.com/docker/compose/releases/download/{self.version}/docker-compose-`uname -s`-`uname -m`" # noqa:501 - c.run(f"sudo curl -sL {link} -o {self.dest}") - c.run(f"sudo chmod a+x {self.dest}") + c.run(f"curl -sL {link} -o {self.dest}") + c.run(f"chmod a+x {self.dest}") print(f"{S.BRIGHT}docker-compose{S.RESET_ALL}: {F.GREEN}already installed{F.RESET}") diff --git a/carnival/contrib/steps/systemd.py b/carnival/contrib/steps/systemd.py index 0a0aab5..950159a 100644 --- a/carnival/contrib/steps/systemd.py +++ b/carnival/contrib/steps/systemd.py @@ -8,7 +8,7 @@ class DaemonReload(Step): """ def run(self, c: Connection) -> None: - c.run("sudo systemctl --system daemon-reload") + c.run("systemctl --system daemon-reload") class Start(Step): @@ -27,7 +27,7 @@ def run(self, c: Connection) -> None: if self.reload_daemon: DaemonReload().run(c=c) - c.run(f"sudo systemctl start {self.service_name}") + c.run(f"systemctl start {self.service_name}") class Stop(Step): @@ -47,7 +47,7 @@ def run(self, c: Connection) -> None: if self.reload_daemon: DaemonReload().run(c=c) - c.run(f"sudo systemctl stop {self.service_name}") + c.run(f"systemctl stop {self.service_name}") class Restart(Step): @@ -62,7 +62,7 @@ def __init__(self, service_name: str) -> None: self.service_name = service_name def run(self, c: Connection) -> None: - c.run(f"sudo systemctl restart {self.service_name}") + c.run(f"systemctl restart {self.service_name}") class Enable(Step): @@ -84,7 +84,7 @@ def run(self, c: Connection) -> None: if self.reload_daemon: DaemonReload().run(c=c) - c.run(f"sudo systemctl enable {self.service_name}") + c.run(f"systemctl enable {self.service_name}") if self.start_now: Start(self.service_name).run(c=c) @@ -110,7 +110,7 @@ def run(self, c: Connection) -> None: if self.reload_daemon: DaemonReload().run(c=c) - c.run(f"sudo systemctl disable {self.service_name}") + c.run(f"systemctl disable {self.service_name}") if self.stop_now: Stop(self.service_name).run(c=c) diff --git a/carnival/hosts/base/connection.py b/carnival/hosts/base/connection.py index ecfb7ec..1d501d0 100644 --- a/carnival/hosts/base/connection.py +++ b/carnival/hosts/base/connection.py @@ -16,15 +16,18 @@ class Connection: Хост с которым связан конект """ - def __init__(self, host: "Host") -> None: + def __init__(self, host: "Host", use_sudo: bool = False) -> None: """ Конекст с хостом, все конекты являются контекст-менеджерами >>> with host.connect() as c: >>> c.run("ls -1") + :param host: хост с которым связано соединение + :param use_sudo: использовать sudo для выполнения команд """ self.host = host + self.use_sudo = use_sudo def __enter__(self) -> "Connection": raise NotImplementedError @@ -34,33 +37,45 @@ def __exit__(self, *args: typing.Any) -> None: @abc.abstractmethod def run_promise( - self, - command: str, - cwd: typing.Optional[str] = None, - timeout: int = 60, + self, + command: str, + use_sudo: bool, + env: typing.Optional[typing.Dict[str, str]] = None, + cwd: typing.Optional[str] = None, + timeout: int = 60, ) -> ResultPromise: raise NotImplementedError def run( - self, - command: str, - hide: bool = True, - warn: bool = False, - cwd: typing.Optional[str] = None, - timeout: int = 60, + self, + command: str, + use_sudo: typing.Optional[bool] = None, + env: typing.Optional[typing.Dict[str, str]] = None, + hide: bool = True, + warn: bool = False, + cwd: typing.Optional[str] = None, + timeout: int = 60, ) -> Result: """ Запустить команду :param command: Команда для запуска + :param use_sudo: использовать sudo для выполнения команды, если не задано используется значение `self.use_sudo` + :param env: задать переменные окружения для команды :param hide: Скрыть вывод команды :param warn: Вывести stderr :param cwd: Перейти в папку при выполнении команды :param timeout: таймаут выполнения команды """ + + if use_sudo is None: + use_sudo = self.use_sudo + result = self.run_promise( command=command, + env=env, cwd=cwd, + use_sudo=use_sudo, timeout=timeout, ).get_result(hide=hide) result.check_result(warn=warn) @@ -69,7 +84,8 @@ def run( @abc.abstractmethod def file_stat(self, path: str) -> StatResult: """ - Получить fstat файла + Получить fstat файла, + не поддерживает `use_sudo` :param path: путь до файла """ @@ -78,6 +94,7 @@ def file_stat(self, path: str) -> StatResult: def file_read(self, path: str) -> typing.ContextManager[typing.IO[bytes]]: """ Открыть файл на чтение + не поддерживает `use_sudo` :param path: путь до файла :return: дескриптор файла @@ -87,6 +104,7 @@ def file_read(self, path: str) -> typing.ContextManager[typing.IO[bytes]]: def file_write(self, path: str) -> typing.ContextManager[typing.IO[bytes]]: """ Открыть файл на запись + не поддерживает `use_sudo` :param path: путь до файла :return: дескриптор файла diff --git a/carnival/hosts/base/host.py b/carnival/hosts/base/host.py index 3e79c0f..bd8fe61 100644 --- a/carnival/hosts/base/host.py +++ b/carnival/hosts/base/host.py @@ -12,6 +12,12 @@ class Host: Адрес хоста """ + def __init__(self, use_sudo: bool = False): + """ + :param use_sudo: использовать sudo для выполнения команд + """ + self.use_sudo = use_sudo + @property def ip(self) -> str: # Maybe self.addr is ip? diff --git a/carnival/hosts/local/connection.py b/carnival/hosts/local/connection.py index 9aef653..4e1b446 100644 --- a/carnival/hosts/local/connection.py +++ b/carnival/hosts/local/connection.py @@ -17,12 +17,16 @@ def __exit__(self, *args: typing.Any) -> None: def run_promise( self, command: str, + use_sudo: bool, + env: typing.Optional[typing.Dict[str, str]] = None, cwd: typing.Optional[str] = None, timeout: int = 60, ) -> LocalResultPromise: return LocalResultPromise( command=command, cwd=cwd, + env=env, + use_sudo=use_sudo, timeout=timeout, ) diff --git a/carnival/hosts/local/result_promise.py b/carnival/hosts/local/result_promise.py index bbbc791..d3f0c32 100644 --- a/carnival/hosts/local/result_promise.py +++ b/carnival/hosts/local/result_promise.py @@ -1,4 +1,5 @@ import typing +import os from subprocess import Popen, PIPE from carnival.hosts.base.result_promise import ResultPromise @@ -7,12 +8,25 @@ class LocalResultPromise(ResultPromise): def __init__( - self, - command: str, - timeout: int, - cwd: typing.Optional[str] + self, + command: str, + timeout: int, + cwd: typing.Optional[str], + use_sudo: bool, + env: typing.Optional[typing.Dict[str, str]] = None, ): - self.proc = Popen(command, shell=True, stderr=PIPE, stdin=PIPE, stdout=PIPE, cwd=cwd) + proc_env = os.environ.copy() + if env is not None: + proc_env.update(env) + + if use_sudo is True: + command = f"sudo -n -- sh -c '{command}'" + + self.proc = Popen( + command, shell=True, + stderr=PIPE, stdin=PIPE, stdout=PIPE, cwd=cwd, + env=proc_env, + ) self.command = command assert self.proc.stdout is not None assert self.proc.stderr is not None diff --git a/carnival/hosts/ssh/__init__.py b/carnival/hosts/ssh/__init__.py index 45a225b..9db516a 100644 --- a/carnival/hosts/ssh/__init__.py +++ b/carnival/hosts/ssh/__init__.py @@ -22,6 +22,7 @@ def __init__( self, addr: str, port: int = SSH_PORT, + use_sudo: bool = False, user: typing.Optional[str] = None, password: typing.Optional[str] = None, @@ -38,6 +39,8 @@ def __init__( :param gateway: Gateway :param connect_timeout: SSH таймаут соединения """ + super(SshHost, self).__init__(use_sudo=use_sudo) + if ":" in addr: raise ValueError("Please set port in 'ssh_port' arg") if "@" in addr: @@ -70,4 +73,5 @@ def connect(self) -> SshConnection: return SshConnection( host=self, conf=self.connect_config, + use_sudo=self.use_sudo, ) diff --git a/carnival/hosts/ssh/connection.py b/carnival/hosts/ssh/connection.py index a702bea..bfe239b 100644 --- a/carnival/hosts/ssh/connection.py +++ b/carnival/hosts/ssh/connection.py @@ -20,8 +20,9 @@ def __init__( self, host: "SshHost", conf: HostnameConfig, + use_sudo: bool = False, ) -> None: - super().__init__(host) + super().__init__(host=host, use_sudo=use_sudo) self.host: "SshHost" = host self.conf = conf self.conn: typing.Optional[SSHClient] = None @@ -40,17 +41,21 @@ def _ensure_connection(self) -> None: self.conn = self.conf.connect() def run_promise( - self, - command: str, - cwd: typing.Optional[str] = None, - timeout: int = 60, + self, + command: str, + use_sudo: bool, + env: typing.Optional[typing.Dict[str, str]] = None, + cwd: typing.Optional[str] = None, + timeout: int = 60, ) -> ResultPromise: self._ensure_connection() assert self.conn is not None, "Connection is not opened" return SshResultPromise( conn=self.conn, command=command, + env=env, cwd=cwd, + use_sudo=use_sudo, timeout=timeout, ) diff --git a/carnival/hosts/ssh/result_promise.py b/carnival/hosts/ssh/result_promise.py index 4385f83..f2e23e4 100644 --- a/carnival/hosts/ssh/result_promise.py +++ b/carnival/hosts/ssh/result_promise.py @@ -6,17 +6,30 @@ class SshResultPromise(ResultPromise): - def __init__(self, conn: SSHClient, command: str, cwd: typing.Optional[str], timeout: int): + def __init__( + self, + conn: SSHClient, + command: str, + cwd: typing.Optional[str], + timeout: int, + use_sudo: bool, + env: typing.Optional[typing.Dict[str, str]] = None, + ): self.command = command self.timeout = timeout self.conn = conn if cwd is not None: command = f"cd {cwd}; {command}" + + if use_sudo is True: + command = f"sudo -n -- sh -c '{command}'" + # https://stackoverflow.com/questions/39429680/python-paramiko-redirecting-stderr-is-affected-by-get-pty-true _, stdout, stderr = self.conn.exec_command( command, timeout=timeout, + environment=env, get_pty=True, # Combines stdout and stderr, we dont want it ) self.stdout_channel = stdout.channel