First working version
This commit is contained in:
131
mtik_cert_pusher/connector.py
Normal file
131
mtik_cert_pusher/connector.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!python3
|
||||
# dlitz 2026
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path, PosixPath
|
||||
|
||||
remote_name_validation_regex = re.compile(
|
||||
r"""
|
||||
\A
|
||||
[A-Za-z0-9_]
|
||||
[A-Za-z0-9_\.\-]*
|
||||
(?:
|
||||
\.
|
||||
[A-Za-z0-9_]+
|
||||
)?
|
||||
\Z
|
||||
""",
|
||||
re.X,
|
||||
)
|
||||
|
||||
|
||||
def _private_opener(file, flags):
|
||||
"""Open a file with restrictive permission bits"""
|
||||
return os.open(os.fsencode(file), flags, 0o600)
|
||||
|
||||
|
||||
class Connector:
|
||||
pass
|
||||
|
||||
|
||||
class SSHConnector(Connector):
|
||||
|
||||
ssh_executable = "ssh"
|
||||
scp_executable = "scp"
|
||||
temporary_directory = "/dev/shm"
|
||||
common_args = ["-oBatchMode=yes", "-oControlMaster=no"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
*,
|
||||
user: str | None = None,
|
||||
port: int | None = None,
|
||||
ssh_config_path: str | None = None,
|
||||
extra_ssh_options=None,
|
||||
extra_ssh_args=None,
|
||||
extra_scp_args=None,
|
||||
):
|
||||
self.ssh_host = host
|
||||
self.ssh_user = user
|
||||
self.ssh_port = port
|
||||
self.ssh_config_path = ssh_config_path
|
||||
if extra_ssh_options is None:
|
||||
extra_ssh_options = {}
|
||||
assert isinstance(extra_ssh_options, dict), "extra_ssh_options should be a dict"
|
||||
self.extra_ssh_options = extra_ssh_options
|
||||
self.extra_ssh_args = extra_ssh_args or ()
|
||||
self.extra_scp_args = extra_scp_args or ()
|
||||
|
||||
def _ssh_option_args(self) -> list:
|
||||
result = []
|
||||
if self.ssh_config_path:
|
||||
result.extend(["-F", self.ssh_config_path])
|
||||
if self.ssh_user:
|
||||
result.append("-oUser={self.ssh_user}")
|
||||
if self.ssh_port:
|
||||
result.append("-oPort={self.ssh_port:d}")
|
||||
for k, v in self.extra_ssh_options.items():
|
||||
assert "=" not in k
|
||||
assert k
|
||||
result.append(f"-o{k}={v}")
|
||||
return result
|
||||
|
||||
def _ssh_args(self, args, /) -> list:
|
||||
return [
|
||||
self.ssh_executable,
|
||||
*self.common_args,
|
||||
*self._ssh_option_args(),
|
||||
*self.extra_ssh_args,
|
||||
*args,
|
||||
]
|
||||
|
||||
def _scp_args(self, args, /) -> list:
|
||||
return [
|
||||
self.scp_executable,
|
||||
*self.common_args,
|
||||
*self._ssh_option_args(),
|
||||
*self.extra_scp_args,
|
||||
*args,
|
||||
]
|
||||
|
||||
def invoke_remote_command(
|
||||
self, cmdline: str, text: bool = False, capture: bool = False
|
||||
) -> str:
|
||||
cmd = self._ssh_args([self.ssh_host, cmdline])
|
||||
#print("running: ", shlex.join(cmd))
|
||||
if capture:
|
||||
return subprocess.check_output(cmd, text=text)
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
def create_remote_files(self, content_by_name: dict, remote_directory: str):
|
||||
if not content_by_name:
|
||||
raise ValueError("require at least one file to copy")
|
||||
with tempfile.TemporaryDirectory(dir=self.temporary_directory, prefix="mtik-connector-tmp") as td:
|
||||
tempfile_paths = []
|
||||
|
||||
# Write the files to a temporary directory
|
||||
for remote_name, content in content_by_name.items():
|
||||
assert isinstance(remote_name, str)
|
||||
if not remote_name_validation_regex.fullmatch(remote_name):
|
||||
raise ValueError(f"illegal remote filename: {remote_name!r}")
|
||||
tempfile_path = Path(td, remote_name)
|
||||
with open(tempfile_path, "wb", opener=_private_opener) as outfile:
|
||||
outfile.write(content)
|
||||
tempfile_paths.append(tempfile_path)
|
||||
|
||||
# Copy them in a single scp command
|
||||
cmd = self._scp_args(
|
||||
[
|
||||
"-q",
|
||||
*(str(p) for p in tempfile_paths),
|
||||
f"{self.ssh_host}:{remote_directory}",
|
||||
]
|
||||
)
|
||||
#print("running: ", shlex.join(cmd))
|
||||
subprocess.run(cmd, check=True)
|
||||
Reference in New Issue
Block a user