Merge ~raharper/curtin:feature/s390x-zkey into curtin:master

Proposed by Ryan Harper
Status: Merged
Approved by: Ryan Harper
Approved revision: 55a46eb11ff0bea6835390d14e867cd7b58cad82
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~raharper/curtin:feature/s390x-zkey
Merge into: curtin:master
Diff against target: 476 lines (+349/-17)
6 files modified
curtin/block/__init__.py (+50/-6)
curtin/commands/block_meta.py (+39/-9)
curtin/commands/curthooks.py (+29/-0)
tests/unittests/test_block.py (+33/-0)
tests/unittests/test_commands_block_meta.py (+174/-0)
tests/unittests/test_curthooks.py (+24/-2)
Reviewer Review Type Date Requested Status
Ryan Harper (community) Approve
Server Team CI bot continuous-integration Approve
Dimitri John Ledkov (community) ship it Approve
Chad Smith Approve
Review via email: mp+368802@code.launchpad.net

Commit message

block: Add opportunistic zkey encryption if supported

On s390x, systems with a crypto accelerator may be present
and enabled for sure. When handling a type: dm_crypt block
configuration, curtin will test if zkey is available and if
so, use the zkey command to generate keys and encrypt the
block device using zkey-based secrets.

In the case that zkey is not available, curtin will fallback
to using normal cryptsetup.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, I'll update with changes.

~raharper/curtin:feature/s390x-zkey updated
534f51b... by Ryan Harper

zkey: add strict boolean to toggle log level in error paths.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

LGTM!thanks for the clarification and fixups.

review: Approve
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

Only minor questions, and a typo to fix.

review: Needs Information
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, I'll fix it up.

~raharper/curtin:feature/s390x-zkey updated
7b4fd22... by Ryan Harper

fix typo in copy_zkey_repository log message.

5d5667b... by Ryan Harper

Only install s390-tools-zkey if zkey_used

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

hahahhahahahahhaha

worse typo ever

hahahhahahahahhaha

review: Approve (ship it)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

vmtest detects the modprobe failure and fails the install even though it went fine.
I need to refactor the logged message when strict=False so that it doesn't trip up
the failure parsing.

~raharper/curtin:feature/s390x-zkey updated
55a46eb... by Ryan Harper

Reword logged error when strict=False to not trip up vmtest error detection.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

https://jenkins.ubuntu.com/server/job/curtin-vmtest-devel-debug/183/console

----------------------------------------------------------------------
Ran 112 tests in 2362.318s

OK
Fri, 21 Jun 2019 20:38:24 +0000: vmtest end [0] in 2365s

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
2index 5d1b1bd..f30c5df 100644
3--- a/curtin/block/__init__.py
4+++ b/curtin/block/__init__.py
5@@ -668,6 +668,24 @@ def get_proc_mounts():
6 return mounts
7
8
9+def _get_dev_disk_by_prefix(prefix):
10+ """
11+ Construct a dictionary mapping devname to disk/<prefix> paths
12+
13+ :returns: Dictionary populated by examining /dev/disk/<prefix>/*
14+
15+ {
16+ '/dev/sda': '/dev/disk/<prefix>/virtio-aaaa',
17+ '/dev/sda1': '/dev/disk/<prefix>/virtio-aaaa-part1',
18+ }
19+ """
20+ return {
21+ os.path.realpath(bypfx): bypfx
22+ for bypfx in [os.path.join(prefix, path)
23+ for path in os.listdir(prefix)]
24+ }
25+
26+
27 def get_dev_disk_byid():
28 """
29 Construct a dictionary mapping devname to disk/by-id paths
30@@ -679,12 +697,7 @@ def get_dev_disk_byid():
31 '/dev/sda1': '/dev/disk/by-id/virtio-aaaa-part1',
32 }
33 """
34-
35- prefix = '/dev/disk/by-id'
36- return {
37- os.path.realpath(byid): byid
38- for byid in [os.path.join(prefix, path) for path in os.listdir(prefix)]
39- }
40+ return _get_dev_disk_by_prefix('/dev/disk/by-id')
41
42
43 def disk_to_byid_path(kname):
44@@ -696,6 +709,15 @@ def disk_to_byid_path(kname):
45 return mapping.get(dev_path(kname))
46
47
48+def disk_to_bypath_path(kname):
49+ """"
50+ Return a /dev/disk/by-path path to kname if present.
51+ """
52+
53+ mapping = _get_dev_disk_by_prefix('/dev/disk/by-path')
54+ return mapping.get(dev_path(kname))
55+
56+
57 def get_device_mapper_links(devpath, first=False):
58 """ Return the best devlink to device at devpath. """
59 info = udevadm_info(devpath)
60@@ -892,6 +914,28 @@ def is_online(device):
61 return int(device_size) > 0
62
63
64+def zkey_supported(strict=True):
65+ """ Return True if zkey cmd present and can generate keys, else False."""
66+ LOG.debug('Checking if zkey encryption is supported...')
67+ try:
68+ util.load_kernel_module('pkey')
69+ except util.ProcessExecutionError as err:
70+ msg = "Failed to load 'pkey' kernel module"
71+ LOG.error(msg + ": %s" % err) if strict else LOG.warning(msg)
72+ return False
73+
74+ try:
75+ with tempfile.NamedTemporaryFile() as tf:
76+ util.subp(['zkey', 'generate', tf.name], capture=True)
77+ LOG.debug('zkey encryption supported.')
78+ return True
79+ except util.ProcessExecutionError as err:
80+ msg = "zkey not supported"
81+ LOG.error(msg + ": %s" % err) if strict else LOG.warning(msg)
82+
83+ return False
84+
85+
86 @contextmanager
87 def exclusive_open(path, exclusive=True):
88 """
89diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
90index 79493fc..a9110a9 100644
91--- a/curtin/commands/block_meta.py
92+++ b/curtin/commands/block_meta.py
93@@ -1062,6 +1062,7 @@ def dm_crypt_handler(info, storage_config):
94 dm_name = info.get('id')
95
96 volume_path = get_path_to_storage_volume(volume, storage_config)
97+ volume_byid_path = block.disk_to_byid_path(volume_path)
98
99 if 'keyfile' in info:
100 if 'key' in info:
101@@ -1077,16 +1078,45 @@ def dm_crypt_handler(info, storage_config):
102 else:
103 raise ValueError("encryption key or keyfile must be specified")
104
105- cmd = ["cryptsetup"]
106- if cipher:
107- cmd.extend(["--cipher", cipher])
108- if keysize:
109- cmd.extend(["--key-size", keysize])
110- cmd.extend(["luksFormat", volume_path, keyfile])
111-
112- util.subp(cmd)
113+ # if zkey is available, attempt to generate and use it; if it's not
114+ # available or fails to setup properly, fallback to normal cryptsetup
115+ # passing strict=False downgrades log messages to warnings
116+ zkey_used = None
117+ if block.zkey_supported(strict=False):
118+ volume_name = "%s:%s" % (volume_byid_path, dm_name)
119+ LOG.debug('Attempting to setup zkey for %s', volume_name)
120+ luks_type = 'luks2'
121+ gen_cmd = ['zkey', 'generate', '--xts', '--volume-type', luks_type,
122+ '--sector-size', '4096', '--name', dm_name,
123+ '--description',
124+ "curtin generated zkey for %s" % volume_name,
125+ '--volumes', volume_name]
126+ run_cmd = ['zkey', 'cryptsetup', '--run', '--volumes',
127+ volume_byid_path, '--batch-mode', '--key-file', keyfile]
128+ try:
129+ util.subp(gen_cmd, capture=True)
130+ util.subp(run_cmd, capture=True)
131+ zkey_used = os.path.join(os.path.split(state['fstab'])[0],
132+ "zkey_used")
133+ # mark in state that we used zkey
134+ util.write_file(zkey_used, "1")
135+ except util.ProcessExecutionError as e:
136+ LOG.exception(e)
137+ msg = 'Setup of zkey on %s failed, fallback to cryptsetup.'
138+ LOG.error(msg % volume_path)
139+
140+ if not zkey_used:
141+ LOG.debug('Using cryptsetup on %s', volume_path)
142+ luks_type = "luks"
143+ cmd = ["cryptsetup"]
144+ if cipher:
145+ cmd.extend(["--cipher", cipher])
146+ if keysize:
147+ cmd.extend(["--key-size", keysize])
148+ cmd.extend(["luksFormat", volume_path, keyfile])
149+ util.subp(cmd)
150
151- cmd = ["cryptsetup", "open", "--type", "luks", volume_path, dm_name,
152+ cmd = ["cryptsetup", "open", "--type", luks_type, volume_path, dm_name,
153 "--key-file", keyfile]
154
155 util.subp(cmd)
156diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
157index 75f5083..2869c6c 100644
158--- a/curtin/commands/curthooks.py
159+++ b/curtin/commands/curthooks.py
160@@ -594,6 +594,28 @@ def copy_zpool_cache(zpool_cache, target):
161 shutil.copy(zpool_cache, os.path.sep.join([target, 'etc/zfs']))
162
163
164+def copy_zkey_repository(zkey_repository, target,
165+ target_repo='etc/zkey/repository'):
166+ if not zkey_repository:
167+ LOG.warn("zkey repository path must be specified, not copying")
168+ return
169+
170+ tdir = os.path.sep.join([target, target_repo])
171+ if not os.path.exists(tdir):
172+ util.ensure_dir(tdir)
173+
174+ files_copied = []
175+ for src in os.listdir(zkey_repository):
176+ source_path = os.path.join(zkey_repository, src)
177+ target_path = os.path.join(tdir, src)
178+ if not os.path.exists(target_path):
179+ shutil.copy2(source_path, target_path)
180+ files_copied.append(target_path)
181+
182+ LOG.debug('Imported zkey repo %s with files: %s',
183+ zkey_repository, files_copied)
184+
185+
186 def apply_networking(target, state):
187 netconf = state.get('network_config')
188 interfaces = state.get('interfaces')
189@@ -1379,6 +1401,13 @@ def builtin_curthooks(cfg, target, state):
190 if os.path.exists(zpool_cache):
191 copy_zpool_cache(zpool_cache, target)
192
193+ zkey_repository = '/etc/zkey/repository'
194+ zkey_used = os.path.join(os.path.split(state['fstab'])[0], "zkey_used")
195+ if all(map(os.path.exists, [zkey_repository, zkey_used])):
196+ distro.install_packages(['s390-tools-zkey'], target=target,
197+ osfamily=osfamily)
198+ copy_zkey_repository(zkey_repository, target)
199+
200 # If a crypttab file was created by block_meta than it needs to be
201 # copied onto the target system, and update_initramfs() needs to be
202 # run, so that the cryptsetup hooks are properly configured on the
203diff --git a/tests/unittests/test_block.py b/tests/unittests/test_block.py
204index c7ebdcc..167a697 100644
205--- a/tests/unittests/test_block.py
206+++ b/tests/unittests/test_block.py
207@@ -712,4 +712,37 @@ class TestGetSupportedFilesystems(CiTestCase):
208 self.assertEqual(0, mock_util.load_file.call_count)
209
210
211+class TestZkeySupported(CiTestCase):
212+
213+ @mock.patch('curtin.block.util')
214+ def test_zkey_supported_loads_module(self, m_util):
215+ block.zkey_supported()
216+ m_util.load_kernel_module.assert_called_with('pkey')
217+
218+ @mock.patch('curtin.block.util.load_kernel_module')
219+ def test_zkey_supported_returns_false_missing_kmod(self, m_kmod):
220+ m_kmod.side_effect = (
221+ util.ProcessExecutionError(stdout=self.random_string(),
222+ stderr=self.random_string(),
223+ exit_code=2))
224+ self.assertFalse(block.zkey_supported())
225+
226+ @mock.patch('curtin.block.util.subp')
227+ @mock.patch('curtin.block.util.load_kernel_module')
228+ def test_zkey_supported_returns_false_zkey_error(self, m_kmod, m_subp):
229+ m_subp.side_effect = (
230+ util.ProcessExecutionError(stdout=self.random_string(),
231+ stderr=self.random_string(),
232+ exit_code=2))
233+ self.assertFalse(block.zkey_supported())
234+
235+ @mock.patch('curtin.block.tempfile.NamedTemporaryFile')
236+ @mock.patch('curtin.block.util')
237+ def test_zkey_supported_calls_zkey_generate(self, m_util, m_temp):
238+ testname = self.random_string()
239+ m_temp.return_value.__enter__.return_value.name = testname
240+ block.zkey_supported()
241+ m_util.subp.assert_called_with(['zkey', 'generate', testname],
242+ capture=True)
243+
244 # vi: ts=4 expandtab syntax=python
245diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
246index b4a9afa..b2e151e 100644
247--- a/tests/unittests/test_commands_block_meta.py
248+++ b/tests/unittests/test_commands_block_meta.py
249@@ -913,4 +913,178 @@ class TestLvmPartitionHandler(CiTestCase):
250 self.assertIn(expected_size_str, call_args[0])
251
252
253+class TestDmCryptHandler(CiTestCase):
254+
255+ def setUp(self):
256+ super(TestDmCryptHandler, self).setUp()
257+
258+ basepath = 'curtin.commands.block_meta.'
259+ self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
260+ self.add_patch(basepath + 'util.load_command_environment',
261+ 'm_load_env')
262+ self.add_patch(basepath + 'util.which', 'm_which')
263+ self.add_patch(basepath + 'util.subp', 'm_subp')
264+ self.add_patch(basepath + 'block', 'm_block')
265+
266+ self.target = "my_target"
267+ self.keyfile = self.random_string()
268+ self.cipher = self.random_string()
269+ self.keysize = self.random_string()
270+ self.config = {
271+ 'storage': {
272+ 'version': 1,
273+ 'config': [
274+ {'grub_device': True,
275+ 'id': 'sda',
276+ 'name': 'sda',
277+ 'path': '/wark/xxx',
278+ 'ptable': 'msdos',
279+ 'type': 'disk',
280+ 'wipe': 'superblock'},
281+ {'device': 'sda',
282+ 'id': 'sda-part1',
283+ 'name': 'sda-part1',
284+ 'number': 1,
285+ 'size': '511705088B',
286+ 'type': 'partition'},
287+ {'id': 'dmcrypt0',
288+ 'type': 'dm_crypt',
289+ 'dm_name': 'cryptroot',
290+ 'volume': 'sda-part1',
291+ 'cipher': self.cipher,
292+ 'keysize': self.keysize,
293+ 'keyfile': self.keyfile},
294+ ],
295+ }
296+ }
297+ self.storage_config = (
298+ block_meta.extract_storage_ordered_dict(self.config))
299+ self.m_block.zkey_supported.return_value = False
300+ self.m_which.return_value = False
301+ self.fstab = self.tmp_path('fstab')
302+ self.crypttab = os.path.join(os.path.dirname(self.fstab), 'crypttab')
303+ self.m_load_env.return_value = {'fstab': self.fstab,
304+ 'target': self.target}
305+
306+ def test_dm_crypt_calls_cryptsetup(self):
307+ """ verify dm_crypt calls (format, open) w/ correct params"""
308+ volume_path = self.random_string()
309+ self.m_getpath.return_value = volume_path
310+
311+ info = self.storage_config['dmcrypt0']
312+ block_meta.dm_crypt_handler(info, self.storage_config)
313+ expected_calls = [
314+ call(['cryptsetup', '--cipher', self.cipher,
315+ '--key-size', self.keysize,
316+ 'luksFormat', volume_path, self.keyfile]),
317+ call(['cryptsetup', 'open', '--type', 'luks', volume_path,
318+ info['dm_name'], '--key-file', self.keyfile])
319+ ]
320+ self.m_subp.assert_has_calls(expected_calls)
321+ self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
322+
323+ def test_dm_crypt_zkey_cryptsetup(self):
324+ """ verify dm_crypt zkey calls generates and run before crypt open."""
325+
326+ # zkey binary is present
327+ self.m_block.zkey_supported.return_value = True
328+ self.m_which.return_value = "/my/path/to/zkey"
329+ volume_path = self.random_string()
330+ self.m_getpath.return_value = volume_path
331+ volume_byid = "/dev/disk/by-id/ccw-%s" % volume_path
332+ self.m_block.disk_to_byid_path.return_value = volume_byid
333+
334+ info = self.storage_config['dmcrypt0']
335+ volume_name = "%s:%s" % (volume_byid, info['dm_name'])
336+ block_meta.dm_crypt_handler(info, self.storage_config)
337+ expected_calls = [
338+ call(['zkey', 'generate', '--xts', '--volume-type', 'luks2',
339+ '--sector-size', '4096', '--name', info['dm_name'],
340+ '--description',
341+ 'curtin generated zkey for %s' % volume_name,
342+ '--volumes', volume_name], capture=True),
343+ call(['zkey', 'cryptsetup', '--run', '--volumes', volume_byid,
344+ '--batch-mode', '--key-file', self.keyfile], capture=True),
345+ call(['cryptsetup', 'open', '--type', 'luks2', volume_path,
346+ info['dm_name'], '--key-file', self.keyfile]),
347+ ]
348+ self.m_subp.assert_has_calls(expected_calls)
349+ self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
350+
351+ def test_dm_crypt_zkey_gen_failure_fallback_to_cryptsetup(self):
352+ """ verify dm_cyrpt zkey generate err falls back cryptsetup format. """
353+
354+ # zkey binary is present
355+ self.m_block.zkey_supported.return_value = True
356+ self.m_which.return_value = "/my/path/to/zkey"
357+
358+ self.m_subp.side_effect = iter([
359+ util.ProcessExecutionError("foobar"), # zkey generate
360+ (0, 0), # cryptsetup luksFormat
361+ (0, 0), # cryptsetup open
362+ ])
363+
364+ volume_path = self.random_string()
365+ self.m_getpath.return_value = volume_path
366+ volume_byid = "/dev/disk/by-id/ccw-%s" % volume_path
367+ self.m_block.disk_to_byid_path.return_value = volume_byid
368+
369+ info = self.storage_config['dmcrypt0']
370+ volume_name = "%s:%s" % (volume_byid, info['dm_name'])
371+ block_meta.dm_crypt_handler(info, self.storage_config)
372+ expected_calls = [
373+ call(['zkey', 'generate', '--xts', '--volume-type', 'luks2',
374+ '--sector-size', '4096', '--name', info['dm_name'],
375+ '--description',
376+ 'curtin generated zkey for %s' % volume_name,
377+ '--volumes', volume_name], capture=True),
378+ call(['cryptsetup', '--cipher', self.cipher,
379+ '--key-size', self.keysize,
380+ 'luksFormat', volume_path, self.keyfile]),
381+ call(['cryptsetup', 'open', '--type', 'luks', volume_path,
382+ info['dm_name'], '--key-file', self.keyfile])
383+ ]
384+ self.m_subp.assert_has_calls(expected_calls)
385+ self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
386+
387+ def test_dm_crypt_zkey_run_failure_fallback_to_cryptsetup(self):
388+ """ verify dm_cyrpt zkey run err falls back on cryptsetup format. """
389+
390+ # zkey binary is present
391+ self.m_block.zkey_supported.return_value = True
392+ self.m_which.return_value = "/my/path/to/zkey"
393+
394+ self.m_subp.side_effect = iter([
395+ (0, 0), # zkey generate
396+ util.ProcessExecutionError("foobar"), # zkey cryptsetup --run
397+ (0, 0), # cryptsetup luksFormat
398+ (0, 0), # cryptsetup open
399+ ])
400+
401+ volume_path = self.random_string()
402+ self.m_getpath.return_value = volume_path
403+ volume_byid = "/dev/disk/by-id/ccw-%s" % volume_path
404+ self.m_block.disk_to_byid_path.return_value = volume_byid
405+
406+ info = self.storage_config['dmcrypt0']
407+ volume_name = "%s:%s" % (volume_byid, info['dm_name'])
408+ block_meta.dm_crypt_handler(info, self.storage_config)
409+ expected_calls = [
410+ call(['zkey', 'generate', '--xts', '--volume-type', 'luks2',
411+ '--sector-size', '4096', '--name', info['dm_name'],
412+ '--description',
413+ 'curtin generated zkey for %s' % volume_name,
414+ '--volumes', volume_name], capture=True),
415+ call(['zkey', 'cryptsetup', '--run', '--volumes', volume_byid,
416+ '--batch-mode', '--key-file', self.keyfile], capture=True),
417+ call(['cryptsetup', '--cipher', self.cipher,
418+ '--key-size', self.keysize,
419+ 'luksFormat', volume_path, self.keyfile]),
420+ call(['cryptsetup', 'open', '--type', 'luks', volume_path,
421+ info['dm_name'], '--key-file', self.keyfile])
422+ ]
423+ self.m_subp.assert_has_calls(expected_calls)
424+ self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
425+
426+
427 # vi: ts=4 expandtab syntax=python
428diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
429index 26a582c..e7d506e 100644
430--- a/tests/unittests/test_curthooks.py
431+++ b/tests/unittests/test_curthooks.py
432@@ -9,7 +9,7 @@ from curtin import distro
433 from curtin import util
434 from curtin import config
435 from curtin.reporter import events
436-from .helpers import CiTestCase, dir2dict
437+from .helpers import CiTestCase, dir2dict, populate_dir
438
439
440 class TestGetFlashKernelPkgs(CiTestCase):
441@@ -201,7 +201,8 @@ class TestInstallMissingPkgs(CiTestCase):
442 cfg = {}
443 curthooks.install_missing_packages(cfg, target=target)
444 self.mock_install_packages.assert_called_with(
445- ['s390-tools'], target=target, osfamily=self.distro_family)
446+ ['s390-tools'], target=target,
447+ osfamily=self.distro_family)
448
449 @patch.object(events, 'ReportEventStack')
450 def test_install_packages_s390x_has_zipl(self, mock_events):
451@@ -1140,4 +1141,25 @@ class TestCurthooksChzdev(CiTestCase):
452 output = curthooks.chzdev_prepare_for_import(self.chzdev_export)
453 self.assertEqual(self.chzdev_import, output)
454
455+
456+class TestCurthooksCopyZkey(CiTestCase):
457+ def setUp(self):
458+ super(TestCurthooksCopyZkey, self).setUp()
459+ self.add_patch('curtin.distro.install_packages', 'mock_instpkg')
460+
461+ self.target = self.tmp_dir()
462+ self.host_dir = self.tmp_dir()
463+ self.zkey_content = {
464+ '/etc/zkey/repository/mykey.info': "key info",
465+ '/etc/zkey/repository/mykey.skey': "key data",
466+ }
467+ self.files = populate_dir(self.host_dir, self.zkey_content)
468+ self.host_zkey = os.path.join(self.host_dir, 'etc/zkey/repository')
469+
470+ def test_copy_zkey_when_dir_present(self):
471+ curthooks.copy_zkey_repository(self.host_zkey, self.target)
472+ found_files = dir2dict(self.target, prefix=self.target)
473+ self.assertEqual(self.zkey_content, found_files)
474+
475+
476 # vi: ts=4 expandtab syntax=python

Subscribers

People subscribed via source and target branches