Merge ~gjolly/ubuntu/+source/sshuttle:merge-1.3.1-1-devel into ubuntu/+source/sshuttle:debian/sid

Proposed by Gauthier Jolly
Status: Needs review
Proposed branch: ~gjolly/ubuntu/+source/sshuttle:merge-1.3.1-1-devel
Merge into: ubuntu/+source/sshuttle:debian/sid
Diff against target: 850 lines (+793/-1)
4 files modified
debian/changelog (+87/-0)
debian/control (+2/-1)
debian/tests/control (+3/-0)
debian/tests/cross-release (+701/-0)
Reviewer Review Type Date Requested Status
Vladimir Petko (community) Abstain
git-ubuntu import Pending
Review via email: mp+483943@code.launchpad.net

Commit message

Manual merge of the package. We were carrying a delta in d/control that is not needed anymore.

To post a comment you must log in.
Revision history for this message
Vladimir Petko (vpa1977) wrote :

Running autopkgtests results in BADPKG since we no longer package lxd:
---

The following packages have unmet dependencies:
 satisfy:command-line : Depends: sshuttle but it is not going to be installed
                        Depends: lxd but it is not installable
E: Unable to correct problems, you have held broken packages.
cross-release FAIL badpkg
blame: arg:sshuttle_1.3.1-1ubuntu1_all.deb deb:sshuttle sshuttle
badpkg: Test dependencies are unsatisfiable. A common reason is that your testbed is out of date with respect to the archive, and you need to use a current testbed or run apt-get update or use -U.
autopkgtest [11:31:45]: @@@@@@@@@@@@@@@@@@@@ summary
cross-release FAIL badpkg

----

review: Abstain
Revision history for this message
Gauthier Jolly (gjolly) wrote :

Unmerged commits

62ff4b4... by Gauthier Jolly

update-maintainer

ec4423b... by Gauthier Jolly

reconstruct-changelog

b92fc59... by Gauthier Jolly

merge-changelogs

083bff7... by Gauthier Jolly

d/t/control,cross-release: Add autopkgtest for cross-release compatibility checks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/debian/changelog b/debian/changelog
index 749f9d2..ccb9464 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,13 @@
1sshuttle (1.3.1-1ubuntu1) questing; urgency=medium
2
3 * Merge with Debian unstable. Remaining changes:
4 - d/t/control,cross-release: Add autopkgtest for cross-release
5 compatibility checks
6 * Drop the removal of python3-distutils from d/control as distutils has also
7 been dropped from the upstream Debian control file.
8
9 -- Gauthier Jolly <contact@gjolly.fr> Fri, 02 May 2025 14:24:08 +0000
10
1sshuttle (1.3.1-1) unstable; urgency=medium11sshuttle (1.3.1-1) unstable; urgency=medium
212
3 * New upstream version.13 * New upstream version.
@@ -37,6 +47,21 @@ sshuttle (1.1.2-1) unstable; urgency=medium
3747
38 -- Brian May <bam@debian.org> Mon, 19 Feb 2024 11:55:11 +110048 -- Brian May <bam@debian.org> Mon, 19 Feb 2024 11:55:11 +1100
3949
50sshuttle (1.1.1-2ubuntu2) noble; urgency=medium
51
52 * Drop dependency on python3-distutils.
53
54 -- Matthias Klose <doko@ubuntu.com> Sat, 09 Mar 2024 12:23:57 +0100
55
56sshuttle (1.1.1-2ubuntu1) noble; urgency=low
57
58 * Merge from Debian unstable. Remaining changes:
59 - d/t/control,cross-release: Add autopkgtest for
60 cross-release compatibility checks
61 * Drop all patches as included in new release.
62
63 -- James Page <james.page@ubuntu.com> Wed, 14 Feb 2024 09:49:52 +0000
64
40sshuttle (1.1.1-2) unstable; urgency=medium65sshuttle (1.1.1-2) unstable; urgency=medium
4166
42 [ Debian Janitor ]67 [ Debian Janitor ]
@@ -60,6 +85,36 @@ sshuttle (1.1.0-1) unstable; urgency=medium
6085
61 -- Brian May <bam@debian.org> Fri, 28 Jan 2022 09:57:26 +110086 -- Brian May <bam@debian.org> Fri, 28 Jan 2022 09:57:26 +1100
6287
88sshuttle (1.0.5-1ubuntu4) jammy; urgency=medium
89
90 * d/p/*use-pty.patch: Cherry-picked from upstream master to fix
91 shuttle permissions failure (LP: #1965829).
92
93 -- Corey Bryant <corey.bryant@canonical.com> Mon, 21 Mar 2022 16:50:35 -0400
94
95sshuttle (1.0.5-1ubuntu3) impish; urgency=medium
96
97 * d/t/cross-release:
98 - fix flakiness and speed of test
99 - install net-tools in testbed instances
100
101 -- Dan Streetman <ddstreet@canonical.com> Wed, 23 Jun 2021 16:34:30 -0400
102
103sshuttle (1.0.5-1ubuntu2) impish; urgency=medium
104
105 * d/t/cross-release: reduce total test time by waiting less
106 for expected timeouts, and fix when we notice sshuttle started
107
108 -- Dan Streetman <ddstreet@canonical.com> Mon, 10 May 2021 10:46:14 -0400
109
110sshuttle (1.0.5-1ubuntu1) hirsute; urgency=medium
111
112 * Merge with Debian; remaining changes:
113 - d/t/control, d/t/cross-release:
114 - add autopkgtest for cross-release compatibility checks
115
116 -- Matthias Klose <doko@ubuntu.com> Tue, 16 Mar 2021 10:25:36 +0100
117
63sshuttle (1.0.5-1) unstable; urgency=medium118sshuttle (1.0.5-1) unstable; urgency=medium
64119
65 * New upstream version.120 * New upstream version.
@@ -67,6 +122,37 @@ sshuttle (1.0.5-1) unstable; urgency=medium
67122
68 -- Brian May <bam@debian.org> Tue, 29 Dec 2020 11:00:34 +1100123 -- Brian May <bam@debian.org> Tue, 29 Dec 2020 11:00:34 +1100
69124
125sshuttle (1.0.4-1ubuntu4) groovy; urgency=medium
126
127 * d/t/cross-release:
128 - test without providing --python param
129
130 -- Dan Streetman <ddstreet@canonical.com> Wed, 30 Sep 2020 17:23:22 -0400
131
132sshuttle (1.0.4-1ubuntu3) groovy; urgency=medium
133
134 * d/t/cross-release:
135 - fixes for autopkgtest
136
137 -- Dan Streetman <ddstreet@canonical.com> Sat, 19 Sep 2020 08:28:03 -0400
138
139sshuttle (1.0.4-1ubuntu2) groovy; urgency=medium
140
141 * d/t/cross-release: fix error in checking sshuttle version
142
143 -- Dan Streetman <ddstreet@canonical.com> Fri, 18 Sep 2020 19:22:08 -0400
144
145sshuttle (1.0.4-1ubuntu1) groovy; urgency=medium
146
147 * d/p/lp1873368/0001-Fix-python2-server-compatibility.patch,
148 d/p/lp1873368/0002-Fix-flake8-line-too-long.patch,
149 d/p/lp1873368/0003-Fix-python2-client-compatibility.patch:
150 - fix compatibility with remote py2 (LP: #1873368)
151 * d/t/control, d/t/cross-release:
152 - add autopkgtest for cross-release compatibility checks
153
154 -- Dan Streetman <ddstreet@canonical.com> Fri, 18 Sep 2020 13:57:01 -0400
155
70sshuttle (1.0.4-1) unstable; urgency=low156sshuttle (1.0.4-1) unstable; urgency=low
71157
72 [ Debian Janitor ]158 [ Debian Janitor ]
@@ -286,3 +372,4 @@ sshuttle (0.42-1) unstable; urgency=low
286 * Write manpage for the Debian release372 * Write manpage for the Debian release
287373
288 -- Javier Fernandez-Sanguino Pen~a <jfs@debian.org> Wed, 27 Oct 2010 02:50:49 +0200374 -- Javier Fernandez-Sanguino Pen~a <jfs@debian.org> Wed, 27 Oct 2010 02:50:49 +0200
375
diff --git a/debian/control b/debian/control
index ccbd6f5..458ac61 100644
--- a/debian/control
+++ b/debian/control
@@ -1,7 +1,8 @@
1Source: sshuttle1Source: sshuttle
2Section: net2Section: net
3Priority: optional3Priority: optional
4Maintainer: Brian May <bam@debian.org>4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
5XSBC-Original-Maintainer: Brian May <bam@debian.org>
5Build-Depends: debhelper-compat (= 13), dh-python,6Build-Depends: debhelper-compat (= 13), dh-python,
6 python3-all, python3-pytest,7 python3-all, python3-pytest,
7 python3-sphinx,8 python3-sphinx,
diff --git a/debian/tests/control b/debian/tests/control
8new file mode 1006449new file mode 100644
index 0000000..42bf2b9
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,3 @@
1Tests: cross-release
2Restrictions: allow-stderr, isolation-machine, needs-root, breaks-testbed, skippable
3Depends: @, lxd, ssh, python3, python3-apt, python3-distro-info
diff --git a/debian/tests/cross-release b/debian/tests/cross-release
0new file mode 1006444new file mode 100644
index 0000000..575d6a4
--- /dev/null
+++ b/debian/tests/cross-release
@@ -0,0 +1,701 @@
1#!/usr/bin/python3
2#
3# This test uses lxd to create a container for each supported Ubuntu release,
4# and test if sshuttle works from the local testbed (using sshhuttle under test)
5# to the remote containter.
6#
7# This also tests the reverse, by creating a container matching the testbed's release,
8# and connecting from each supported Ubuntu release's container. Note that the reverse
9# direction tests *do not* test the sshuttle under test by this autopkgtest, since
10# on a "remote" system sshuttle is not involved at all, and does not even need to be
11# installed; the reverse direction test primarily tests if anything *else* has changed
12# that breaks the *existing* sshuttle on the instance (most likely, changes in python).
13
14import apt_pkg
15import functools
16import ipaddress
17import json
18import os
19import re
20import sys
21import subprocess
22import tempfile
23import time
24import unittest
25
26from aptsources.distro import get_distro
27from contextlib import suppress
28from distro_info import UbuntuDistroInfo
29from pathlib import Path
30
31
32DISTROINFO = UbuntuDistroInfo()
33VALID_RELEASES = set(DISTROINFO.supported_esm() + DISTROINFO.supported() + [DISTROINFO.devel()])
34RELEASES = []
35TESTBED = None
36SSH_KEY = None
37SSH_CONFIG = None
38IF_NAME = 'eth1'
39BR_NAME = 'br1'
40
41# really silly that users need to call this...especially just to use version_compare
42apt_pkg.init_system()
43
44class Testbed(object):
45 def __init__(self):
46 self.net = {}
47 self.index = {}
48 self._find_subnets()
49
50 @property
51 def shared_glob(self):
52 '''Get the 10.X.* glob-format subnet for ~/.ssh/config usage'''
53 b = self.net.get('shared').exploded.split('.')[1]
54 return f'10.{b}.0.*'
55
56 @property
57 def shared_next(self):
58 '''This is the next unique ip address on the shared subnet'''
59 return self._next_interface('shared')
60
61 @property
62 def remote(self):
63 '''This is the unique private subnet used for each "remote" instance'''
64 return self._interface('remote')
65
66 @property
67 def reverse(self):
68 '''This is the unique private subnet used for the single "reverse remote" instance'''
69 return self._interface('reverse')
70
71 def _network(self, b):
72 return ipaddress.ip_network(f'10.{b}.0.0/24')
73
74 def _network_to_interface(self, network, index=0):
75 addr = list(network.hosts())[index].exploded
76 return ipaddress.ip_interface(f'{addr}/{network.prefixlen}')
77
78 def _interface(self, name, index=0):
79 return self._network_to_interface(self.net.get(name), index)
80
81 def _next_interface(self, name):
82 index = self.index.get(name)
83 self.index[name] = index + 1
84 return self._interface(name, index)
85
86 def _find_subnets(self):
87 start = 254
88 for name in ['shared', 'remote', 'reverse']:
89 start = self._find_subnet(name, start)
90
91 def _find_subnet(self, name, start):
92 for b in range(start, 0, -1):
93 network = self._network(b)
94 iface = self._network_to_interface(network)
95 result = subprocess.run(f'ip r get {iface.ip}'.split(),
96 encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
97 # we try to find a subnet that isn't locally reachable
98 # if returncode != 0, then the subnet isn't reachable (i.e. no gateway)
99 # if 'via' is in stdout, then the subnet isn't locally reachable
100 if result.returncode != 0 or 'via' in result.stdout:
101 self.net[name] = network
102 self.index[name] = 0
103 return b - 1
104 else:
105 raise Exception('Could not find any 10.* subnet to use for private addresses')
106
107@functools.lru_cache
108def get_arch():
109 return run_cmd('dpkg --print-architecture').stdout.strip()
110
111def is_expected_failure(src, dst, python):
112 if not python and is_expected_failure_nopy(src, dst):
113 return True
114 if python == 'python2' and is_expected_failure_py2(src, dst):
115 return True
116 if python == 'python3' and is_expected_failure_py3(src, dst):
117 return True
118
119 # otherwise, we don't expect failure
120 return False
121
122def is_expected_failure_nopy(src, dst):
123 # failure due to regression in patch to detect python command
124 # should be fixed in version after this; LP: #1897961
125 if src.release == 'xenial' and apt_pkg.version_compare(src.sshuttle_version, '0.76-1ubuntu1.1') <= 0:
126 return True
127
128def is_expected_failure_py2(src, dst):
129 # failure due to regression from initial fix for py3.8 fix
130 # should be fixed in version after this; LP: #1873368
131 if src.release == 'focal' and apt_pkg.version_compare(src.sshuttle_version, '0.78.5-1ubuntu1') <= 0:
132 return True
133
134def is_expected_failure_py3(src, dst):
135 # expected failure: trusty -> any
136 # since trusty is now ESM only, this isn't expected to be fixed
137 if src.release == 'trusty':
138 return True
139
140 # failure with py3.8 (or later) target, which is default py3 in focal (or later)
141 if DISTROINFO.version(dst.release) >= DISTROINFO.version('focal'):
142 # should be fixed in version after each of these; LP: #1873368
143 if src.release == 'xenial' and apt_pkg.version_compare(src.sshuttle_version, '0.76-1ubuntu1') <= 0:
144 return True
145 if src.release == 'bionic' and apt_pkg.version_compare(src.sshuttle_version, '0.78.3-1ubuntu1') <= 0:
146 return True
147 if src.release == 'focal' and apt_pkg.version_compare(src.sshuttle_version, '0.78.5-1ubuntu1') <= 0:
148 return True
149
150 # otherwise, we don't expect failure
151 return False
152
153def set_releases(releases):
154 invalid_releases = list(set(releases) - VALID_RELEASES)
155 if invalid_releases:
156 print(f'ignoring invalid release(s): {", ".join(invalid_releases)}')
157 valid_releases = list(set(releases) & VALID_RELEASES)
158 if valid_releases:
159 print(f'limiting remote release(s) to: {", ".join(valid_releases)}')
160 RELEASES.clear()
161 RELEASES.extend(valid_releases)
162
163def load_tests(loader, standard_tests, pattern):
164 suite = unittest.TestSuite()
165 for release in sorted(RELEASES or VALID_RELEASES):
166 cls = type(f'SshuttleTest_{release}', (SshuttleTest,),
167 {'release': release})
168 suite.addTests(loader.loadTestsFromTestCase(cls))
169 return suite
170
171def setUpModule():
172 global TESTBED
173
174 _run_cmd('lxd init --auto', check=True)
175
176 TESTBED = Testbed()
177
178 add_shared_bridge()
179 add_private_subnets()
180 init_ssh_config()
181 init_base_test_class()
182
183def tearDownModule():
184 remove_ssh_config()
185 remove_private_subnets()
186 remove_shared_bridge()
187 del SshuttleTest.reverse_remote
188
189def add_shared_bridge():
190 _run_cmd(f'ip l add dev {BR_NAME} type bridge')
191 _run_cmd(f'ip l set up dev {BR_NAME}')
192 _run_cmd(f'ip a add {TESTBED.shared_next} dev {BR_NAME}')
193
194def add_private_subnets():
195 # Force the private addrs unreachable so we don't try to reach them out our normal gateway
196 _run_cmd(f'ip r add {TESTBED.remote.network} dev lo')
197 _run_cmd(f'ip r add {TESTBED.reverse.network} dev lo')
198
199def remove_private_subnets():
200 _run_cmd(f'ip r del {TESTBED.remote.network} dev lo')
201 _run_cmd(f'ip r del {TESTBED.reverse.network} dev lo')
202
203def remove_shared_bridge():
204 _run_cmd(f'ip l del dev {BR_NAME}')
205
206def init_ssh_config():
207 global SSH_KEY
208 global SSH_CONFIG
209
210 id_rsa = Path('/root/.ssh/id_rsa')
211 if not id_rsa.exists():
212 _run_cmd(['ssh-keygen', '-f', str(id_rsa), '-P', ''], check=True)
213 SSH_KEY = id_rsa.with_suffix('.pub').read_text(encoding='utf-8')
214
215 SSH_CONFIG = '\n'.join([f'Host {TESTBED.remote.ip} {TESTBED.reverse.ip} {TESTBED.shared_glob}',
216 ' StrictHostKeyChecking no',
217 ' UserKnownHostsFile /dev/null',
218 ' ConnectTimeout 10',
219 ' ConnectionAttempts 18',
220 ''])
221 config = Path('/root/.ssh/config')
222 if config.exists():
223 content = config.read_text(encoding='utf-8') or ''
224 if content and not content.endswith('\n'):
225 content += '\n'
226 else:
227 content = ''
228 content += SSH_CONFIG
229 config.write_text(content, encoding='utf-8')
230
231def remove_ssh_config():
232 config = Path('/root/.ssh/config')
233 content = config.read_text(encoding='utf-8')
234 config.write_text(content.replace(SSH_CONFIG, ''), encoding='utf-8')
235
236def init_base_test_class():
237 cls = SshuttleTest
238
239 cls.release = get_distro().codename
240 reverse_remote = Remote(f'reverse-remote-{cls.release}', cls.release)
241 reverse_remote.add_ssh_key(SSH_KEY)
242 reverse_remote.add_ssh_config(SSH_CONFIG)
243 reverse_remote.private = TESTBED.reverse
244 reverse_remote.add_start_cmd(f'ip a add {reverse_remote.private} dev lo')
245 reverse_remote.snapshot_create()
246 cls.reverse_remote = reverse_remote
247
248def _run_cmd(cmd, **kwargs):
249 if type(cmd) == str:
250 cmd = cmd.split()
251 return subprocess.run(cmd, **kwargs)
252
253def run_cmd(cmd, **kwargs):
254 kwargs.setdefault('stdout', subprocess.PIPE)
255 kwargs.setdefault('stderr', subprocess.STDOUT)
256 kwargs.setdefault('encoding', 'utf-8')
257 return _run_cmd(cmd, **kwargs)
258
259
260class Remote(object):
261 def __init__(self, name, release):
262 self.name = name
263 self.release = release
264 self.shared = TESTBED.shared_next
265 self._start_cmds = []
266
267 cmd = f'lxc delete --force {self.name}'
268 self.log(cmd)
269 run_cmd(cmd)
270
271 image = f'ubuntu-daily:{release}'
272 cmd = f'lxc launch --quiet {image} {self.name}'
273 self.log(cmd)
274 result = run_cmd(cmd)
275 if result.returncode != 0:
276 raise Exception(f'Could not launch {self.name}: {result.stdout}')
277
278 cmd = f'lxc config device add {self.name} {IF_NAME} nic name={IF_NAME} nictype=bridged parent={BR_NAME}'
279 self.log(cmd)
280 result = run_cmd(cmd)
281 if result.returncode != 0:
282 raise Exception(f'Could not add {IF_NAME}: {result.stdout}')
283
284 self.add_start_cmd(f'ip l set up dev {IF_NAME}')
285 self.add_start_cmd(f'ip a add {self.shared} dev {IF_NAME}')
286
287 self._wait_for_networking()
288 self._create_ssh_key()
289 self._add_local_ppas()
290 self._add_proposed()
291 self._apt_update_upgrade()
292 self._install_net_tools()
293 self._install_sshuttle()
294 self._install_python()
295 self.stop(force=False)
296
297 def log(self, msg):
298 print(f'{self.name}: {msg}')
299
300 def save_journal(self, testname, remotename):
301 artifacts_dir = os.getenv('AUTOPKGTEST_ARTIFACTS')
302 if not artifacts_dir:
303 self.log('AUTOPKGTEST_ARTIFACTS unset, not saving container journal')
304 return
305
306 dst = Path(artifacts_dir) / testname / remotename
307 dst.mkdir(parents=True, exist_ok=True)
308
309 self.lxc_exec('journalctl --sync --flush')
310 self.lxc_file_pull('/var/log/journal', dst, recursive=True)
311
312 def _wait_for_networking(self):
313 self.log(f'Waiting for {self.name} to finish starting')
314 for sec in range(120):
315 if 'via' in self.lxc_exec('ip r show default').stdout:
316 break
317 time.sleep(0.5)
318 else:
319 raise Exception(f'Timed out waiting for remote {self.name} networking')
320
321 def _create_ssh_key(self):
322 self.log('creating ssh key')
323 self.lxc_exec(['ssh-keygen', '-f', '/root/.ssh/id_rsa', '-P', ''])
324 self._ssh_key = self.lxc_exec('cat /root/.ssh/id_rsa.pub').stdout
325
326 def _add_local_ppas(self):
327 paths = list(Path('/etc/apt/sources.list.d').glob('*.list'))
328 paths.append(Path('/etc/apt/sources.list'))
329 ppas = []
330 for path in paths:
331 for line in path.read_text(encoding='utf-8').splitlines():
332 match = re.match(r'^deb .*ppa.launchpad.net/(?P<team>\w+)/(?P<ppa>\w+)/ubuntu', line)
333 if match:
334 ppas.append(f'ppa:{match.group("team")}/{match.group("ppa")}')
335 for ppa in ppas:
336 self.log(f'adding PPA {ppa}')
337 self.lxc_exec(['add-apt-repository', '-y', ppa])
338
339 def _add_proposed(self):
340 with tempfile.TemporaryDirectory() as d:
341 f = Path(d) / 'tempfile'
342 self.lxc_file_pull('/etc/apt/sources.list', str(f))
343 for line in f.read_text(encoding='utf-8').splitlines():
344 match = re.match(rf'^deb (?P<uri>\S+) {self.release} main.*', line)
345 if match:
346 uri = match.group('uri')
347 components = 'man universe restricted multiverse'
348 proposed_line = f'deb {uri} {self.release}-proposed {components}'
349 self.log(f'adding {self.release}-proposed using {uri}')
350 self.lxc_exec(['add-apt-repository', '-y', proposed_line])
351 return
352
353 def _apt_update_upgrade(self):
354 self.log('upgrading packages')
355 self.lxc_apt('update')
356 self.lxc_apt('upgrade -y')
357
358 def _install_net_tools(self):
359 self.log('installing net-tools')
360 result_install = self.lxc_apt('install -y net-tools')
361 result_which = self.lxc_exec('which netstat')
362 if result_which.returncode != 0:
363 err = result_install.stdout + result_which.stdout
364 raise Exception(f'could not install net-tools: {err}')
365
366 def _install_sshuttle(self):
367 self.log('installing sshuttle')
368 result_install = self.lxc_apt('install -y sshuttle')
369 result_which = self.lxc_exec('which sshuttle')
370 if result_which.returncode != 0:
371 err = result_install.stdout + result_which.stdout
372 raise Exception(f'could not install sshuttle: {err}')
373 self.sshuttle_version = self.lxc_exec('dpkg-query -f ${Version} -W sshuttle').stdout
374
375 def _install_python(self):
376 self.log('installing python')
377 self.lxc_apt('install -y python')
378 for python in ['python2', 'python3']:
379 result_install = self.lxc_apt(['install', '-y', python])
380 result_which = self.lxc_exec(['which', python])
381 if result_which.returncode != 0:
382 err = result_install.stdout + result_which.stdout
383 raise Exception(f'could not install {python}: {err}')
384
385 def snapshot_create(self, name='default'):
386 self.log(f'creating snapshot: {name}')
387 self.stop(force=False)
388 subprocess.run(['lxc', 'snapshot', self.name, name], check=True)
389
390 def snapshot_restore(self, name='default', start=True):
391 self.log(f'restoring snapshot: {name}')
392 self.stop()
393 subprocess.run(['lxc', 'restore', self.name, name], check=True)
394 if start:
395 self.start()
396
397 def snapshot_update(self, name='default'):
398 self.log(f'updating snapshot: {name}')
399 subprocess.run(['lxc', 'delete', '--force', f'{self.name}/{name}'], check=True)
400 self.snapshot_create(name)
401
402 @functools.cached_property
403 def ssh_key(self):
404 return self._ssh_key
405
406 def add_start_cmd(self, cmd):
407 self.log(f'adding start cmd: {cmd}')
408 self._start_cmds.append(cmd)
409
410 def add_file_content(self, path, content):
411 with tempfile.TemporaryDirectory() as d:
412 localfile = Path(d) / Path(path).name
413 self.lxc_file_pull(path, str(localfile))
414 existing_content = localfile.read_text(encoding='utf-8') or ''
415 if content not in existing_content:
416 if existing_content and not existing_content.endswith('\n'):
417 existing_content += '\n'
418 existing_content += content
419 localfile.write_text(existing_content)
420 self.lxc_file_push(str(localfile), path)
421
422 def add_ssh_key(self, key):
423 self.log(f'adding ssh key: {key.strip()}')
424 self.add_file_content('/root/.ssh/authorized_keys', key)
425
426 def add_ssh_config(self, config):
427 self.log('adding ssh config')
428 self.add_file_content('/root/.ssh/config', config)
429
430 def lxc_exec(self, cmd, **kwargs):
431 if type(cmd) == str:
432 cmd = cmd.split()
433 return run_cmd(['lxc', 'exec', self.name, '--'] + cmd, **kwargs)
434
435 def lxc_apt(self, cmd, **kwargs):
436 if type(cmd) == str:
437 cmd = cmd.split()
438 return run_cmd(['lxc', 'exec', self.name, '--env', 'DEBIAN_FRONTEND=noninteractive', '--', 'apt'] + cmd, **kwargs)
439
440 def lxc_file_pull(self, remote, local, fail_if_missing=False, recursive=False):
441 remote = f'{self.name}{remote}'
442 self.log(f'{local} <- {remote}')
443 cmd = ['lxc', 'file', 'pull', remote, local]
444 if recursive:
445 cmd += ['--recursive', '--create-dirs']
446 try:
447 run_cmd(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
448 except subprocess.CalledProcessError:
449 if fail_if_missing:
450 raise
451 if recursive:
452 self.log('remote dir missing, ignoring')
453 return
454 localpath = Path(local)
455 if localpath.is_dir():
456 localpath = localpath / Path(remote).name
457 localpath.touch()
458 self.log(f'remote file missing, created empty file {localpath}')
459
460 def lxc_file_push(self, local, remote):
461 remote = f'{self.name}{remote}'
462 self.log(f'{local} -> {remote}')
463 run_cmd(['lxc', 'file', 'push', local, remote], check=True)
464
465 @property
466 def json(self):
467 listjson = run_cmd('lxc list --format json').stdout
468 filtered = list(filter(lambda i: i['name'] == self.name, json.loads(listjson)))
469 if len(filtered) != 1:
470 raise Exception(f'Expected only 1 lxc list entry for {self.name}, found {len(filtered)}:\n{listjson}')
471 return filtered[0]
472
473 @property
474 def is_running(self):
475 return self.json['status'] == 'Running'
476
477 def start(self):
478 if not self.is_running:
479 cmd = f'lxc start {self.name}'
480 self.log(cmd)
481 result = run_cmd(cmd, check=True)
482 if result.stdout:
483 self.log(result.stdout)
484 self._wait_for_networking()
485 for cmd in self._start_cmds:
486 self.lxc_exec(cmd)
487
488 def stop(self, force=True):
489 cmd = 'lxc stop'
490 if force:
491 cmd += ' --force'
492 cmd += f' {self.name}'
493 self.log(cmd)
494 result = run_cmd(cmd)
495 if result.stdout:
496 self.log(result.stdout)
497
498 def __del__(self):
499 run_cmd(['lxc', 'delete', '--force', self.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
500
501
502class VerboseAssertionError(AssertionError):
503 __logs = []
504
505 def __init__(self, *args):
506 logs = list(args) + self.read_log()
507 super(VerboseAssertionError, self).__init__('\n'.join(logs))
508
509 @classmethod
510 def add_log(cls, msg):
511 cls.__logs.append(str(msg))
512
513 @classmethod
514 def read_log(cls):
515 log = cls.__logs
516 cls.__logs = []
517 return log
518
519 @classmethod
520 def clear_log(cls):
521 cls.read_log()
522
523
524class SshuttleTest(unittest.TestCase):
525 release = None
526 failureException = VerboseAssertionError
527
528 @classmethod
529 def is_arch_supported(cls):
530 if cls.release == 'trusty':
531 return get_arch() == 'amd64'
532 return True
533
534 @classmethod
535 def setUpClass(cls):
536 # note that some of the cls attrs used here are set by setUpModule()
537
538 # this is set by the subclass, and required
539 assert(cls.release)
540
541 if not cls.is_arch_supported():
542 raise unittest.SkipTest(f'Release {cls.release} not available for {get_arch()}')
543
544 remote = Remote(f'remote-{cls.release}', cls.release)
545 remote.add_ssh_key(SSH_KEY)
546 remote.add_ssh_config(SSH_CONFIG)
547 remote.private = TESTBED.remote
548 remote.add_start_cmd(f'ip a add {remote.private} dev lo')
549 remote.snapshot_create()
550 cls.remote = remote
551
552 cls.reverse_remote.snapshot_restore()
553 cls.reverse_remote.add_ssh_key(cls.remote.ssh_key)
554 cls.reverse_remote.snapshot_update()
555
556 @classmethod
557 def tearDownClass(cls):
558 del cls.remote
559
560 def setUp(self):
561 self.name = f'testbed-{self.release}'
562 self.reverse_remote.snapshot_restore()
563 self.remote.snapshot_restore()
564 self.sshuttle_process = None
565 self.sshuttle_log = tempfile.NamedTemporaryFile()
566 self.failureException.clear_log()
567
568 def tearDown(self):
569 self.sshuttle_stop()
570 self.sshuttle_log.close()
571 self.remote.stop()
572 self.reverse_remote.stop()
573
574 @functools.cached_property
575 def sshuttle_version(self):
576 return run_cmd('dpkg-query -f ${Version} -W sshuttle').stdout
577
578 def sshuttle_started_ok(self):
579 output = Path(self.sshuttle_log.name).read_text(encoding='utf-8')
580 # Unfortunately we have to just grep the output to see if it 'connected'
581 # and the specific output format has changed across versions
582 # Since all output so far includes 'Connected' we'll use that word
583 return 'connected' in output.lower()
584
585 def sshuttle_start(self, dst, python):
586 sshuttle_cmd = 'sshuttle'
587 if python:
588 sshuttle_cmd += f' --python {python}'
589 sshuttle_cmd += f' -r {dst.shared.ip} {dst.private.network}'
590 if dst is self.reverse_remote:
591 sshuttle_cmd = f'lxc exec {self.remote.name} -- {sshuttle_cmd}'
592 print(f'running: {sshuttle_cmd}')
593 self.sshuttle_process = subprocess.Popen(sshuttle_cmd.split(), encoding='utf-8',
594 stdout=self.sshuttle_log, stderr=self.sshuttle_log)
595 print('waiting for sshuttle to start...', end='', flush=True)
596 for sec in range(300):
597 if self.sshuttle_process.poll() is not None:
598 print('sshuttle failed :-(', flush=True)
599 break
600 if self.sshuttle_started_ok():
601 print('started', flush=True)
602 break
603 time.sleep(1)
604 print('.', end='', flush=True)
605 else:
606 print("WARNING: timed out waiting for sshuttle to start, the test may fail")
607 if self.sshuttle_process.poll() is not None:
608 self.fail('sshuttle process failed to start')
609
610 def sshuttle_stop(self):
611 if self.sshuttle_process and self.sshuttle_process.poll() is None:
612 print('stopping sshuttle...')
613 self.sshuttle_process.terminate()
614 with suppress(subprocess.TimeoutExpired):
615 self.sshuttle_process.communicate(timeout=30)
616 print('sshuttle stopped')
617 self.sshuttle_process = None
618 return
619
620 print('sshuttle did not respond, killing sshuttle...')
621 self.sshuttle_process.kill()
622 with suppress(subprocess.TimeoutExpired):
623 self.sshuttle_process.communicate(timeout=30)
624 print('sshuttle stopped')
625 self.sshuttle_process = None
626 return
627
628 self.fail('sshuttle subprocess refused to stop')
629
630 def ssh_to(self, remote, expect_timeout=False):
631 ssh_cmd = 'ssh'
632 if expect_timeout:
633 # No need to wait long if we expect it to timeout
634 ssh_cmd += ' -o ConnectionAttempts=2'
635 ssh_cmd += f' -v {remote.private.ip} -- cat /proc/sys/kernel/hostname'
636 if remote is self.reverse_remote:
637 ssh_cmd = f'lxc exec {self.remote.name} -- {ssh_cmd}'
638 print(f'running: {ssh_cmd}')
639 result = run_cmd(ssh_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
640 failed = result.returncode != 0
641 if failed:
642 if result.stderr:
643 # just print the last line here
644 print(result.stderr.splitlines()[-1])
645 if failed != expect_timeout:
646 self.failureException.add_log(result.stdout)
647 self.failureException.add_log(result.stderr)
648 elif result.stdout:
649 print(f'Connected to: {result.stdout.strip()}')
650 msg = 'ssh'
651 if failed:
652 msg += ' failed'
653 if expect_timeout:
654 msg += ' (as expected)'
655 else:
656 msg += ' connected'
657 print(msg, flush=True)
658 return not failed
659
660 def test_local_to_remote_nopy(self):
661 self._test_to_remote(self, self.remote, None)
662
663 def test_local_to_remote_py2(self):
664 self._test_to_remote(self, self.remote, 'python2')
665
666 def test_local_to_remote_py3(self):
667 self._test_to_remote(self, self.remote, 'python3')
668
669 def test_remote_to_reverse_remote_nopy(self):
670 self._test_to_remote(self.remote, self.reverse_remote, None)
671
672 def test_remote_to_reverse_remote_py2(self):
673 self._test_to_remote(self.remote, self.reverse_remote, 'python2')
674
675 def test_remote_to_reverse_remote_py3(self):
676 self._test_to_remote(self.remote, self.reverse_remote, 'python3')
677
678 def _test_to_remote(self, src, dst, python):
679 self.failureException.add_log(f'Test detail: {src.name} sshuttle {src.sshuttle_version} to {dst.name} {python if python else ""}')
680 print('this ssh connection should timeout:')
681 self.assertFalse(self.ssh_to(dst, expect_timeout=True))
682 try:
683 self.sshuttle_start(dst, python)
684 print('this ssh connection should not timeout:')
685 self.assertTrue(self.ssh_to(dst))
686 except AssertionError:
687 if is_expected_failure(src, dst, python):
688 self.skipTest('This is an expected failure, ignoring test failure')
689 else:
690 self.failureException.add_log(Path(self.sshuttle_log.name).read_text(encoding='utf-8'))
691 testname = '.'.join(self.id().split('.')[-2:])
692 self.remote.save_journal(testname, 'remote')
693 self.reverse_remote.save_journal(testname, 'reverse_remote')
694 raise
695
696
697if __name__ == '__main__':
698 if len(sys.argv) > 1:
699 set_releases(sys.argv[1:])
700 del sys.argv[1:]
701 unittest.main(verbosity=2)

Subscribers

People subscribed via source and target branches