Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ssh connector #3

Merged
merged 2 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading