Skip to content

Commit fdf2241

Browse files
authored
Merge pull request #3 from mkalcok/ssh-connector
Ssh connector
2 parents 6872b15 + 36c8032 commit fdf2241

File tree

12 files changed

+505
-19
lines changed

12 files changed

+505
-19
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,12 @@ Press 'Enter' to rebuild and deploy OVN. (Ctrl-C for exit)
195195
## Supported remote connectors
196196

197197
This tool is primarily meant to sync OVN binaries to the remote hosts running the
198-
cluster. Following connectors are currently supported:
199-
* LXD
198+
cluster. It supports multiple types of connection types called "Connectors". Each
199+
connector has specific syntax for defining remote targets expected in the `-H/--host`
200+
argument. Following connectors are currently supported:
201+
202+
* LXD - `lxd:<container_name>`
203+
* SSH - `ssh:[<username>@]<hostname_or_ip>`
200204

201205
## Caveats
202206

@@ -250,7 +254,6 @@ Note that this command may "fail" if the coverage is not sufficient.
250254
* Support execution of arbitrary scripts aside from just restarting services on file
251255
updates. This will enable syncing things like OVSDB schemas as they require migration
252256
execution of a migration script instead of just a service restart.
253-
* Add SSH connector
254257
* Add automation for bootstrapping local OVN source repository
255258
* Add automation for bootstrap remote cluster
256259
* Add command that lists supported remote connectors

microovn_rebuilder/cli.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def watch(
5959
print("[local] No changes in watched files")
6060
except KeyboardInterrupt:
6161
print()
62+
connector.teardown()
6263
break
6364

6465

@@ -81,10 +82,13 @@ def parse_args() -> argparse.Namespace: # pragma: no cover
8182
)
8283
parser.add_argument(
8384
"-H",
84-
"--host",
85+
"--hosts",
8586
type=str,
8687
required=True,
87-
help="Remote host specification. Expected format: '<connection_type>:<remote_host>'",
88+
help="Comma-separated list of remote host to which changes will be synced. For "
89+
"details on supported connectors and their syntax, please see "
90+
"documentation. Generally, the format is:"
91+
"'<connection_type>:<remote_host>'",
8892
)
8993
parser.add_argument(
9094
"-j",
@@ -106,7 +110,7 @@ def main() -> None:
106110
sys.exit(1)
107111

108112
try:
109-
connector = create_connector(args.host)
113+
connector = create_connector(args.hosts)
110114
connector.check_remote(args.remote_path)
111115
except ConnectorException as exc:
112116
print(f"Failed to create connection to remote host: {exc}")

microovn_rebuilder/remote/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from .base import BaseConnector, ConnectorException
22
from .lxd import LXDConnector
3+
from .ssh import SSHConnector
34

4-
_CONNECTORS = {"lxd": LXDConnector}
5+
_CONNECTORS = {
6+
"lxd": LXDConnector,
7+
"ssh": SSHConnector,
8+
}
59

610

711
def create_connector(remote_spec: str) -> BaseConnector:
@@ -23,10 +27,13 @@ def create_connector(remote_spec: str) -> BaseConnector:
2327
)
2428

2529
connector_type = types.pop()
26-
connector = _CONNECTORS.get(connector_type)
27-
if connector is None:
30+
connector_class = _CONNECTORS.get(connector_type)
31+
if connector_class is None:
2832
raise ConnectorException(
2933
f"{connector_type} is not a valid connector type. Available types: {", ".join(_CONNECTORS.keys())}"
3034
)
3135

32-
return connector(remotes)
36+
connector = connector_class(remotes)
37+
connector.initialize()
38+
39+
return connector

microovn_rebuilder/remote/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ class BaseConnector(ABC):
1313
def __init__(self, remotes: List[str]) -> None:
1414
self.remotes = remotes
1515

16+
@abstractmethod
17+
def initialize(self) -> None:
18+
pass # pragma: no cover
19+
20+
@abstractmethod
21+
def teardown(self) -> None:
22+
pass # pragma: no cover
23+
1624
@abstractmethod
1725
def check_remote(self, remote_dst: str) -> None:
1826
pass # pragma: no cover

microovn_rebuilder/remote/lxd.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
class LXDConnector(BaseConnector):
1212

13+
def initialize(self) -> None:
14+
# LXDConnector does not require any special initialization
15+
pass # pragma: no cover
16+
17+
def teardown(self) -> None:
18+
# LXDConnector does not require any special teardown
19+
pass # pragma: no cover
20+
1321
def update(self, target: Target) -> None:
1422
for remote in self.remotes:
1523
print(f"{os.linesep}[{remote}] Removing remote file {target.remote_path}")

microovn_rebuilder/remote/ssh.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import os
2+
from typing import Dict, List
3+
4+
from paramiko import SSHClient, SSHException
5+
6+
from microovn_rebuilder.remote.base import BaseConnector, ConnectorException
7+
from microovn_rebuilder.target import Target
8+
9+
10+
class SSHConnector(BaseConnector):
11+
def __init__(self, remotes: List[str]) -> None:
12+
super().__init__(remotes=remotes)
13+
14+
self.connections: Dict[str, SSHClient] = {}
15+
16+
def initialize(self) -> None:
17+
for remote in self.remotes:
18+
username, found, host = remote.partition("@")
19+
try:
20+
ssh = SSHClient()
21+
ssh.load_system_host_keys()
22+
if found:
23+
ssh.connect(hostname=host, username=username)
24+
else:
25+
ssh.connect(hostname=remote)
26+
self.connections[remote] = ssh
27+
except SSHException as exc:
28+
raise ConnectorException(
29+
f"Failed to connect to {remote}: {exc}"
30+
) from exc
31+
32+
def update(self, target: Target) -> None:
33+
for remote, ssh in self.connections.items():
34+
try:
35+
with ssh.open_sftp() as sftp:
36+
local_stat = os.stat(str(target.local_path))
37+
print(
38+
f"{os.linesep}[{remote}] Removing remote file {target.remote_path}"
39+
)
40+
sftp.remove(str(target.remote_path))
41+
42+
print(
43+
f"[{remote}] Uploading file {target.local_path} to {target.remote_path}"
44+
)
45+
sftp.put(target.local_path, str(target.remote_path))
46+
sftp.chmod(str(target.remote_path), local_stat.st_mode)
47+
if target.service:
48+
print(f"[{remote}] Restarting {target.service}")
49+
self._run_command(ssh, remote, f"snap restart {target.service}")
50+
except SSHException as exc:
51+
raise ConnectorException(
52+
f"[{remote}] Failed to upload file: {exc}"
53+
) from exc
54+
55+
def check_remote(self, remote_dst: str) -> None:
56+
for remote, ssh in self.connections.items():
57+
self._run_command(ssh, remote, f"test -d {remote_dst}")
58+
59+
@staticmethod
60+
def _run_command(ssh: SSHClient, remote: str, command: str) -> None:
61+
try:
62+
_, stdout, stderr = ssh.exec_command(command)
63+
ret_code = stdout.channel.recv_exit_status()
64+
if ret_code != 0:
65+
error = stderr.read().decode("utf-8")
66+
raise ConnectorException(
67+
f"[{remote}] Failed to execute command: {error}]"
68+
)
69+
except SSHException as exc:
70+
raise ConnectorException(
71+
f"[{remote}] Failed to execute command: {exc}"
72+
) from exc
73+
74+
def teardown(self) -> None:
75+
for host, ssh in self.connections.items():
76+
ssh.close()
77+
self.connections.clear()

0 commit comments

Comments
 (0)