Skip to content

Commit

Permalink
Merge pull request #3 from mkalcok/ssh-connector
Browse files Browse the repository at this point in the history
Ssh connector
  • Loading branch information
mkalcok authored Dec 25, 2024
2 parents 6872b15 + 36c8032 commit fdf2241
Show file tree
Hide file tree
Showing 12 changed files with 505 additions and 19 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,12 @@ Press 'Enter' to rebuild and deploy OVN. (Ctrl-C for exit)
## Supported remote connectors

This tool is primarily meant to sync OVN binaries to the remote hosts running the
cluster. Following connectors are currently supported:
* LXD
cluster. It supports multiple types of connection types called "Connectors". Each
connector has specific syntax for defining remote targets expected in the `-H/--host`
argument. Following connectors are currently supported:

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

## Caveats

Expand Down Expand Up @@ -250,7 +254,6 @@ Note that this command may "fail" if the coverage is not sufficient.
* Support execution of arbitrary scripts aside from just restarting services on file
updates. This will enable syncing things like OVSDB schemas as they require migration
execution of a migration script instead of just a service restart.
* Add SSH connector
* Add automation for bootstrapping local OVN source repository
* Add automation for bootstrap remote cluster
* Add command that lists supported remote connectors
Expand Down
10 changes: 7 additions & 3 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 All @@ -81,10 +82,13 @@ def parse_args() -> argparse.Namespace: # pragma: no cover
)
parser.add_argument(
"-H",
"--host",
"--hosts",
type=str,
required=True,
help="Remote host specification. Expected format: '<connection_type>:<remote_host>'",
help="Comma-separated list of remote host to which changes will be synced. For "
"details on supported connectors and their syntax, please see "
"documentation. Generally, the format is:"
"'<connection_type>:<remote_host>'",
)
parser.add_argument(
"-j",
Expand All @@ -106,7 +110,7 @@ def main() -> None:
sys.exit(1)

try:
connector = create_connector(args.host)
connector = create_connector(args.hosts)
connector.check_remote(args.remote_path)
except ConnectorException as exc:
print(f"Failed to create connection to remote host: {exc}")
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 fdf2241

Please sign in to comment.