Skip to content

Commit

Permalink
remotes: Add SSH connector.
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Kalcok <martin.kalcok@gmail.com>
  • Loading branch information
mkalcok committed Dec 25, 2024
1 parent e25bc7e commit 03257ec
Show file tree
Hide file tree
Showing 12 changed files with 490 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ connector has specific syntax for defining remote targets expected in the `-H/-
argument. Following connectors are currently supported:

* LXD - `lxd:<container_name>`
* SSH - `ssh:[<username>@]<hostname_or_ip>`

## Caveats

Expand Down
1 change: 1 addition & 0 deletions microovn_rebuilder/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def watch(
print("[local] No changes in watched files")
except KeyboardInterrupt:
print()
connector.teardown()
break


Expand Down
15 changes: 11 additions & 4 deletions microovn_rebuilder/remote/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from .base import BaseConnector, ConnectorException
from .lxd import LXDConnector
from .ssh import SSHConnector

_CONNECTORS = {"lxd": LXDConnector}
_CONNECTORS = {
"lxd": LXDConnector,
"ssh": SSHConnector,
}


def create_connector(remote_spec: str) -> BaseConnector:
Expand All @@ -23,10 +27,13 @@ def create_connector(remote_spec: str) -> BaseConnector:
)

connector_type = types.pop()
connector = _CONNECTORS.get(connector_type)
if connector is None:
connector_class = _CONNECTORS.get(connector_type)
if connector_class is None:
raise ConnectorException(
f"{connector_type} is not a valid connector type. Available types: {", ".join(_CONNECTORS.keys())}"
)

return connector(remotes)
connector = connector_class(remotes)
connector.initialize()

return connector
8 changes: 8 additions & 0 deletions microovn_rebuilder/remote/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ class BaseConnector(ABC):
def __init__(self, remotes: List[str]) -> None:
self.remotes = remotes

@abstractmethod
def initialize(self) -> None:
pass # pragma: no cover

@abstractmethod
def teardown(self) -> None:
pass # pragma: no cover

@abstractmethod
def check_remote(self, remote_dst: str) -> None:
pass # pragma: no cover
Expand Down
8 changes: 8 additions & 0 deletions microovn_rebuilder/remote/lxd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

class LXDConnector(BaseConnector):

def initialize(self) -> None:
# LXDConnector does not require any special initialization
pass # pragma: no cover

def teardown(self) -> None:
# LXDConnector does not require any special teardown
pass # pragma: no cover

def update(self, target: Target) -> None:
for remote in self.remotes:
print(f"{os.linesep}[{remote}] Removing remote file {target.remote_path}")
Expand Down
77 changes: 77 additions & 0 deletions microovn_rebuilder/remote/ssh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import os
from typing import Dict, List

from paramiko import SSHClient, SSHException

from microovn_rebuilder.remote.base import BaseConnector, ConnectorException
from microovn_rebuilder.target import Target


class SSHConnector(BaseConnector):
def __init__(self, remotes: List[str]) -> None:
super().__init__(remotes=remotes)

self.connections: Dict[str, SSHClient] = {}

def initialize(self) -> None:
for remote in self.remotes:
username, found, host = remote.partition("@")
try:
ssh = SSHClient()
ssh.load_system_host_keys()
if found:
ssh.connect(hostname=host, username=username)
else:
ssh.connect(hostname=remote)
self.connections[remote] = ssh
except SSHException as exc:
raise ConnectorException(
f"Failed to connect to {remote}: {exc}"
) from exc

def update(self, target: Target) -> None:
for remote, ssh in self.connections.items():
try:
with ssh.open_sftp() as sftp:
local_stat = os.stat(str(target.local_path))
print(
f"{os.linesep}[{remote}] Removing remote file {target.remote_path}"
)
sftp.remove(str(target.remote_path))

print(
f"[{remote}] Uploading file {target.local_path} to {target.remote_path}"
)
sftp.put(target.local_path, str(target.remote_path))
sftp.chmod(str(target.remote_path), local_stat.st_mode)
if target.service:
print(f"[{remote}] Restarting {target.service}")
self._run_command(ssh, remote, f"snap restart {target.service}")
except SSHException as exc:
raise ConnectorException(
f"[{remote}] Failed to upload file: {exc}"
) from exc

def check_remote(self, remote_dst: str) -> None:
for remote, ssh in self.connections.items():
self._run_command(ssh, remote, f"test -d {remote_dst}")

@staticmethod
def _run_command(ssh: SSHClient, remote: str, command: str) -> None:
try:
_, stdout, stderr = ssh.exec_command(command)
ret_code = stdout.channel.recv_exit_status()
if ret_code != 0:
error = stderr.read().decode("utf-8")
raise ConnectorException(
f"[{remote}] Failed to execute command: {error}]"
)
except SSHException as exc:
raise ConnectorException(
f"[{remote}] Failed to execute command: {exc}"
) from exc

def teardown(self) -> None:
for host, ssh in self.connections.items():
ssh.close()
self.connections.clear()
Loading

0 comments on commit 03257ec

Please sign in to comment.