Skip to content

Commit b818cef

Browse files
Merge pull request #124 from craigcomstock/ENT-12668
ENT-12668: Fix nt-discovery.sh usage and other improvements
2 parents 49b2e95 + 33a5f39 commit b818cef

File tree

11 files changed

+118
-43
lines changed

11 files changed

+118
-43
lines changed

.github/workflows/python-tests.yml renamed to .github/workflows/tests.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
33

4-
name: Python tests
4+
name: Tests
55

66
on:
77
push:
@@ -10,7 +10,7 @@ on:
1010
branches: [ master ]
1111

1212
jobs:
13-
test:
13+
python_version:
1414
runs-on: ubuntu-20.04
1515
env:
1616
# Temporary workaround for Python 3.5 failures - May 2024, see CFE-4395
@@ -48,7 +48,9 @@ jobs:
4848
pip install dist/cf_remote-*.whl
4949
- name: Sanity check
5050
run: cf-remote -V
51-
# No bash tests yet, see: https://github.com/cfengine/cfbs
52-
# - name: Run bash tests
53-
# run: |
54-
# UNSAFE_TESTS=1 bash test/shell/all.sh
51+
- name: Run docker test
52+
run: |
53+
bash tests/docker/0*.sh
54+
- name: Run unsafe tests
55+
run: |
56+
bash tests/unsafe/0*.sh

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include cf_remote/nt-discovery.sh

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ Commands for provisioning hosts in the cloud (AWS or GCP) are also available.
66

77
## Requirements
88

9-
cf-remote requires python 3.6 or greater.
10-
SSH must already be configured and running on the remote host.
9+
- cf-remote requires python 3.6 or greater.
10+
- SSH must be configured in such a way that cf-remote can login without a password.
11+
- An sftp server for transferring files on UNIX hosts. e.g. openssh-sftp-server for debian-based distributions.
1112

1213
## Installation
1314

@@ -22,6 +23,10 @@ $ pip3 install cf-remote
2223
### See information about remote host
2324

2425
The `info` command can be used to check basic information about a system.
26+
The --hosts/-H option accepts [user@]hostname[:port] for the hostname.
27+
In the case that hostname is an ipv6 address use literal square brackets as described in RFC-3986 (https://www.ietf.org/rfc/rfc3986.txt)
28+
29+
e.g. user@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8022
2530

2631
```
2732
$ cf-remote info -H 34.241.203.218

cf_remote/aramid.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from collections import namedtuple
2828
import subprocess
2929
import time
30+
from urllib.parse import urlparse
3031

3132
DEFAULT_SSH_ARGS = [
3233
"-oLogLevel=ERROR",
@@ -130,6 +131,7 @@ def communicate(self, timeout=1, ignore_failed=False):
130131
raise _TaskError("Failed to communicate with the process") from e
131132
else:
132133
if self.proc.returncode == 255: # SSH error
134+
self.stderr += err
133135
if self._retries > 0:
134136
# wait for the rest of timeout (if any) and restart the process
135137
time.sleep(max(timeout - (time.time() - start), 0))
@@ -152,7 +154,10 @@ def communicate(self, timeout=1, ignore_failed=False):
152154
% (self.host.host_name, self._max_retries)
153155
)
154156
else:
155-
raise _TaskError("SSH failed on '%s'" % self.host.host_name)
157+
raise _TaskError(
158+
"SSH failed on '%s' with error '%s'"
159+
% (self.host.host_name, self.stderr)
160+
)
156161
else:
157162
self.done = True
158163
self.stdout += out
@@ -191,22 +196,20 @@ def get_result(self):
191196
class Host:
192197
"""A remote host to execute commands on or copy files to"""
193198

194-
def __init__(self, host_name, user="root", extra_ssh_args=None):
199+
def __init__(
200+
self, host_name, user="root", port=_DEFAULT_SSH_PORT, extra_ssh_args=None
201+
):
195202
"""
196203
:param str host_name: host name or IP of the host
197204
:param str user: user name to use to login to the host
198205
:param extra_ssh_args: extra SSH arguments to use when opening an SSH
199206
connection to the host
200207
201208
"""
202-
if ":" in host_name:
203-
host_name, port = host_name.split(":")
204-
self.host_name = host_name
205-
self.port = int(port)
206-
else:
207-
self.host_name = host_name
208-
self.port = _DEFAULT_SSH_PORT
209-
self.user = user
209+
parts = urlparse("ssh://%s" % host_name)
210+
self.user = user or parts.username
211+
self.port = port or parts.port
212+
self.host_name = parts.hostname
210213
self.extra_ssh_args = extra_ssh_args or []
211214

212215
self.tasks = []
File renamed without changes.

cf_remote/remote.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
import sys
33
import re
4-
from os.path import basename
4+
from os.path import basename, dirname, join, exists
55
from collections import OrderedDict
66

77
from cf_remote.utils import (
@@ -208,7 +208,16 @@ def get_info(host, *, users=None, connection=None):
208208
else:
209209
data["os"] = "unix"
210210

211-
scp("nt-discovery.sh", host, connection, hide=True)
211+
cf_remote_dir = dirname(__file__)
212+
script_path = join(cf_remote_dir, "nt-discovery.sh")
213+
if not exists(script_path):
214+
sys.exit("%s does not exist" % script_path)
215+
scp(
216+
script_path,
217+
host,
218+
connection,
219+
hide=True,
220+
)
212221
discovery = parse_envfile(ssh_sudo(connection, "bash nt-discovery.sh"))
213222

214223
if discovery is None:
@@ -255,7 +264,6 @@ def get_info(host, *, users=None, connection=None):
255264

256265
@auto_connect
257266
def install_package(host, pkg, data, *, connection=None):
258-
259267
print("Installing: '{}' on '{}'".format(pkg, host))
260268
output = None
261269
if ".deb" in pkg:
@@ -478,7 +486,6 @@ def install_host(
478486
remote_download=False,
479487
trust_keys=None
480488
):
481-
482489
data = get_info(host, connection=connection)
483490
if show_info:
484491
print_info(data)

cf_remote/ssh.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import shutil
55
import signal
66
import subprocess
7+
from urllib.parse import urlparse
78

89
from cf_remote import aramid
910
from cf_remote import log
@@ -30,22 +31,29 @@ def run(self, command, hide=False):
3031
stderr=subprocess.STDOUT,
3132
shell=True,
3233
universal_newlines=True,
34+
cwd=os.environ["HOME"],
3335
)
3436
result.retcode = result.returncode
3537
return result
3638

3739
def put(self, src, hide=False):
40+
dst = os.path.join(os.environ["HOME"], os.path.basename(src))
3841
src = os.path.abspath(src)
39-
dst = os.path.basename(src)
4042
if src != dst:
4143
if not hide:
4244
print("Local copy: '%s' -> '%s'" % (src, dst))
4345
shutil.copy(src, dst)
4446

4547

4648
class Connection:
47-
def __init__(self, host, user, connect_kwargs=None):
49+
def __init__(self, host, user, connect_kwargs=None, port=aramid._DEFAULT_SSH_PORT):
50+
log.debug(
51+
"Initializing Connection: host '%s' user '%s' port '%s'"
52+
% (host, user, port)
53+
)
54+
4855
self.ssh_host = host
56+
self.ssh_port = port
4957
self.ssh_user = user
5058
self._connect_kwargs = connect_kwargs
5159

@@ -56,16 +64,22 @@ def __init__(self, host, user, connect_kwargs=None):
5664
"ssh",
5765
"-M",
5866
"-N",
67+
"-p %s" % self.ssh_port,
5968
"-oControlPath=%s" % self._control_path,
6069
]
6170
control_master_args.extend(aramid.DEFAULT_SSH_ARGS)
62-
control_master_args.append("%s@%s" % (user, host))
71+
control_master_args.append("%s@%s" % (self.ssh_user, self.ssh_host))
6372

73+
log.debug(
74+
"Attempting to open SSH Control Master process with command: %s"
75+
% " ".join(control_master_args)
76+
)
6477
self._ssh_control_master = subprocess.Popen(
65-
control_master_args
78+
control_master_args,
6679
) # stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
6780

6881
self.needs_sudo = self.run("echo $UID", hide=True).stdout.strip() != "0"
82+
log.debug("Connection initialized")
6983

7084
def __del__(self):
7185
# If we have an SSH Control Master running, signal it to terminate.
@@ -83,16 +97,17 @@ def run(self, command, hide=False):
8397
# If the Control Master process is running (poll() returns None), let's
8498
# reuse its connection.
8599
if self._ssh_control_master.poll() is None:
100+
log.debug("Control Master is running, using it")
86101
extra_ssh_args.extend(["-oControlPath=%s" % self._control_path])
87102

88-
ahost = aramid.Host(self.ssh_host, self.ssh_user, extra_ssh_args)
103+
ahost = aramid.Host(self.ssh_host, self.ssh_user, self.ssh_port, extra_ssh_args)
89104
results = aramid.execute([ahost], command, echo=(not hide))
90105
return results[ahost][0]
91106

92107
def put(self, src, hide=False):
93108
dst = os.path.basename(src)
94-
ahost = aramid.Host(self.ssh_host, self.ssh_user)
95-
results = aramid.put([ahost], src, dst, echo=(not hide))
109+
ahost = aramid.Host(self.ssh_host, self.ssh_user, self.ssh_port)
110+
results = aramid.put([ahost], src, dst=dst, echo=(not hide))
96111
return results[ahost][0].retcode
97112

98113
def __enter__(self, *args, **kwargs):
@@ -105,12 +120,11 @@ def __exit__(self, *args, **kwargs):
105120
def connect(host, users=None):
106121
log.debug("Connecting to '%s'" % host)
107122
log.debug("users= '%s'" % users)
108-
if "@" in host:
109-
parts = host.split("@")
110-
assert len(parts) == 2
111-
host = parts[1]
112-
if not users:
113-
users = [parts[0]]
123+
parts = urlparse("ssh://%s" % host)
124+
host = parts.hostname
125+
if not users:
126+
users = [parts.username]
127+
port = parts.port or aramid._DEFAULT_SSH_PORT
114128
if not users:
115129
users = [
116130
"Administrator",
@@ -127,14 +141,17 @@ def connect(host, users=None):
127141
users = [whoami()] + users
128142
for user in users:
129143
try:
130-
log.debug("Attempting ssh: %s@%s" % (user, host))
144+
log.debug("Attempting ssh: %s@%s:%s" % (user, host, port))
131145
connect_kwargs = {}
132146
key = os.getenv("CF_REMOTE_SSH_KEY")
133147
if key:
134148
connect_kwargs["key_filename"] = os.path.expanduser(key)
135-
c = Connection(host=host, user=user, connect_kwargs=connect_kwargs)
149+
c = Connection(
150+
host=host, user=user, port=port, connect_kwargs=connect_kwargs
151+
)
136152
c.ssh_user = user
137153
c.ssh_host = host
154+
c.ssh_port = port
138155
c.run("whoami", hide=True)
139156
return c
140157
except aramid.ExecutionError:

tests/aws-spawn-test.sh

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ trap cleanup EXIT
1010

1111
# this is a fairly exhaustive test and will take some time
1212
# spawn all reasonable "platform" specifications
13-
function test() {
13+
function test_spawn() {
1414
platform=$1
1515
version=$2
1616

@@ -46,17 +46,18 @@ for platform in debian rhel windows debian-9 ubuntu-22 centos-7 rhel-9 windows-2
4646
cleanup
4747
done
4848
for version in 9 10 11 12; do
49-
test debian $version
49+
test_spawn debian $version
5050
done
5151
for version in 7 8; do
52-
test centos $version
52+
test_spawn centos $version
5353
done
5454
for version in 7 8 9; do
55-
test rhel $version
55+
test_spawn rhel $version
5656
done
5757
for version in 2008 2012 2016 2019 2022; do
58-
test windows $version
58+
test_spawn windows $version
5959
done
6060
for version in 16-04 18-04 20-04 22-04; do
61-
test ubuntu "$version"
61+
test_spawn ubuntu "$version"
6262
done
63+
test_spawn alpine
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
set -ex
3+
set -o pipefail
4+
5+
rm -f log
6+
error () {
7+
echo "=== error occurred, rc=$?, logs follow ==="
8+
[ -f log ] && cat log
9+
}
10+
trap error ERR
11+
12+
dir=$(dirname "$0")
13+
name=cf-remote-debian-test-host
14+
15+
docker stop "$name" || true
16+
docker rm "$name" || true
17+
docker build -t "$name" "$dir" >log 2>&1
18+
docker run -d -p 8822:22 --name "$name" "$name" >>log 2>&1
19+
ip_addr=$(hostname -i)
20+
ssh -o StrictHostKeyChecking=no -p 8822 root@"$ip_addr" hostname >>log 2>&1
21+
echo "ssh returned exit code $?"
22+
echo "=== cf-remote info ===" | tee -a log
23+
cf-remote --log-level DEBUG info -H root@"$ip_addr":8822 2>&1 | tee -a log
24+
echo "cf-remote info got return code $?"
25+
echo "=== cf-remote install ===" | tee -a log
26+
cf-remote --log-level DEBUG install --clients root@"$ip_addr":8822 2>&1 | tee -a log
27+
ssh -o StrictHostKeyChecking=no -p 8822 root@"$ip_addr" cf-agent -V >>log 2>&1

tests/docker/Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM debian:12-slim
2+
3+
# openssh-server depends on dbus and systemd which is a lot, so use dropbear instead
4+
RUN apt update -y && apt install -y dropbear openssh-sftp-server
5+
6+
# setup no password login via ssh for root
7+
RUN sed -ie 's/^root:[^:]*:/root::/' /etc/shadow
8+
9+
# run dropbear sshd in foreground (-F) and allow blank password logins (-B)
10+
CMD [ "dropbear", "-FB" ]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cf-remote install --clients localhost
2+
cf-agent -V

0 commit comments

Comments
 (0)