Merge lp:~jtv/maas-test/bootresources-config into lp:maas-test

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 161
Merged at revision: 148
Proposed branch: lp:~jtv/maas-test/bootresources-config
Merge into: lp:maas-test
Diff against target: 358 lines (+171/-16)
7 files modified
maastest/kvmfixture.py (+2/-1)
maastest/maasfixture.py (+42/-5)
maastest/testing/factory.py (+7/-0)
maastest/tests/test_kvmfixture.py (+8/-2)
maastest/tests/test_maasfixture.py (+104/-4)
maastest/tests/test_utils.py (+4/-1)
maastest/utils.py (+4/-3)
To merge this branch: bzr merge lp:~jtv/maas-test/bootresources-config
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Raphaël Badin (community) Needs Fixing
Review via email: mp+214916@code.launchpad.net

Commit message

Generate bootresources.yaml and upload it to the VM before importing images.

Description of the change

This should make the import script import just the images that are required. This includes i386/generic images as well as those for the requested architecture, all for the requested Ubuntu release series.

At least one change is completely unrelated: a utils test was in the wrong TestCase. I gave it a TestCase of its own. Along the way I also added another utility function to the test factory module.

I initially planned to upload the bootresources config into the ubuntu user's home directory on the VM, and then use the import script's --config option to load it; but Raphaël mentioned that he might like to replace the script invocation with an API call, and then I found that just uploading to /etc/maas/ wasn't much easier anyway. It's an extra sudo invocation, but it saves us having to pass that --config option. The complications for existing tests weren't so bad.

One thing that is not covered by tests, though I don't expect it will matter, is the fact that import_maas_images passes the architectures it gets from mipf_arch_list (not in __all__, so I added it, then actually sorted __all__) and not just [self.architecture].

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

[0]

Looks like this doesn't work. I extracted the generated bootresources.yaml generated by this change (http://d-jenkins.ubuntu-ci:8080/view/MAAS/job/maas-test-manual/60/console) and ran the import script manually on canonistack and got http://paste.ubuntu.com/7225943/.

[1]

fwiw, I disagree with the approach taken here. I understand it's been approved by Julian so I won't block this but I really think this is wrong.

Having maas-test generate bootresources.yaml instead of just taking its content from a command line argument will force us to change maas-test every single time we will want to get it to tweak bootresources.yaml: to use a different label, to tweak the keyring or the simplestream path. This is the same mistake we've done with the power parameters settings. maas-test should try to do as little as possible and just pass things around, act as a dumb intermediate whenever possible.

[2]

47 + self.kvm_fixture.upload_file(config.name, 'bootresources.yaml')
48 + self.kvm_fixture.run_command(
49 + ['sudo', 'mv', 'bootresources.yaml', '/etc/maas/'],
50 + check_call=True)

Shouldn't we put the file in /tmp/bootresources.yaml? This looks like it will work just fine but maybe it's best to have the entire path under our control, just to be on the safe side.

[3]

> This includes i386/generic images as well as those for the requested architecture, all for the requested Ubuntu
> release series.

This was https://bugs.launchpad.net/ubuntu/+source/maas/+bug/1181334 and it's fixed now. Since maas-test can only be used to run MAAS 1.5 and up, we might as well be rid of this and avoid downloading the i386 image when testing an amd64 node. I know it has nothing to do with you branch per se, I'm just mentioning it here so that we don't forget.

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (6.6 KiB)

Raphaël and I had a long conversation about this. Ultimately we agreed
that this branch out to be put on hold, and we should discuss it
properly in Austin in a few weeks. Can you live with that Jeroen?

Transcript:

allenap: rvba: I think bootresources.yaml is different to power
         parameters. The latter have a number of schemas associated with
         them. Trying to flatten that into a command-line is nearly
         impossible.
   rvba: allenap: my suggestion is to pass the entire file content to
         maas-test.
   rvba: All at once.
allenap: rvba: bootresources.yaml will be transient, but we can use
         capabilities to detect when we should use its successor.
   rvba: allenap: maas-test --bootresources <file>
allenap: rvba: Would that be optional?
   rvba: allenap: yes, of course
allenap: rvba: What’s the purpose?
   rvba: allenap: customize the boot image import: use a different
         label, use a different path, etc
allenap: rvba: I imagine we still need the code in this branch when
         it’s not supplied?
   rvba: allenap: no, we can use the default one.
allenap: rvba: Does that import everything? Using the default one makes
         sense - we’re trying to test how hardware works with MAAS
         out-of-the-box - but if it’s really slow then folk are going
         to tire of it.
allenap: rvba: This is pertinent w.r.t. the “directive” we had about
         default-to-everything.
   rvba: allenap: It imports [i386,amd64]/release/precise
         [i386/amd64]/daily/trusty by default.
allenap: rvba: Is that MAAS’s default, or maas-test’s?
   rvba: MAAS's default
allenap: rvba: When we transition away from bootresources.yaml, what
         will we do with maas-test then?
   rvba: allenap: maybe we won't transition away from
         bootresources.yaml.  Maybe the region will simply generate an
         ad-hoc bootresources.yaml every time the import script it run.
   rvba: allenap: and if we do get away with it, then that's one more
         reason no to spend time getting maas-test capable of writing
         that file.
allenap: rvba: Either way, bootresources.yaml is an implementation
         detail, temporarily exposed to users while we scurry to provide
         API+UI for it. Jeroen has written the code to generate it. I
         think we should use it for now. I suspect we’ll have to
         change maas-test again in any case.
   rvba: allenap: well, I don't agree but I guess I'm outnumbered :)
   rvba: allenap: bootresources.yaml being an implementation detail
         should be yet another reason not to spend time working on code
         to generate it.  We should expose it to user temporarily to
         give them maximum control over what gets imported.  And change
         that when MAAS itself changes.
allenap: rvba: I think we can revisit this at some point. In the
         meantime I think jtv’s branch is an improvement, and is okay
         to land.
   rvba: allenap: we won't be able to offer a unified interface in maas-
         test anyway.
   rvba: allenap: well, apart from the fact that it doesn't work at all.
allenap: rvba: Doesn’t it?
   rvba: allenap: see my point [0]
allenap: rvba: Oh yes...

Read more...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 09 Apr 2014 12:19:25 you wrote:
> fwiw, I disagree with the approach taken here. I understand it's been
> approved by Julian so I won't block this but I really think this is wrong.
>
> Having maas-test generate bootresources.yaml instead of just taking its
> content from a command line argument will force us to change maas-test
> every single time we will want to get it to tweak bootresources.yaml: to
> use a different label, to tweak the keyring or the simplestream path. This
> is the same mistake we've done with the power parameters
> settings. maas-test should try to do as little as possible and just pass
> things around, act as a dumb intermediate whenever possible.

bootresources.yaml is going to disappear very soon, so I advised jtv to take
the route that involved the least amount of work.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 09 Apr 2014 14:11:32 you wrote:
> allenap: rvba: I don’t know what Julian said, but I’m not thrilled
> about adding it either. It depends somewhat on how you see
> maas-test. I see it as a front-end for non-MAAS users to test
> hardware with MAAS. As such, I don’t think asking them to
> provide config files is good UI.

This is the nub of it, yes.

bootresources.yaml is a shit user-facing interface, I mean really shit. I was
rather frustrated to see the simplestreams filter options appearing in a
config file in the original script work, but that was out of our hands as it
turns out.

We need to be moving as much of the maas-test functionality as possible over
to maas itself so that maas-test ends up being a wrapper to drive some tests
inside maas.

When we have an API/UI to control which boot resources are required (which we
will design with maas-test in mind) then this all becomes much easier.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This is madness. We want to make people download gigabytes extra in the release version because we're arguing over a hundred lines of code we could save with a temporary solution with an unfriendly UI that is also doomed from the start? I'm not buying it.

I agree that just passing a bootresources.yaml would be easier (and take about half the code, not “10× less”) but that is not the desired user experience. The difference in code size really isn't that dramatic in absolute numbers, for a user-friendliness issue.

Also, it should be noted for the record that the branch did work properly in the experimental run, but because of it, revealed an outdated default setting in MAAS itself (which was fixed within the hour after Gavin pasted the conversation). That, not a problem with my branch, was what stopped the experiment before it could be completed — but by that point my changes had already completed their work, and MAAS had already accepted it. So far as we know or have reason to expect, no fixes to my branch are needed.

Julian says it's acceptable to keep working with the release images (which does mean maas-test will break until Trusty's release) and add an option to use pre-release images later, using a simpler user interface than letting the user configure paths and labels. It's not very hard, and doesn't differ much from the work we would need to do to pass a configuration file in from the command line.

Our chosen direction is to abolish bootresources.yaml. That file is universally regarded as a problem. We can execute on that plan, or we can keep second-guessing it, but either way my branch does not stand in its way. On the other hand, introducing bootresources.yaml in full detail into the user interface is committing publicly to the status quo.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 10 Apr 2014 03:36:53 you wrote:
> Our chosen direction is to abolish bootresources.yaml. That file is
> universally regarded as a problem. We can execute on that plan, or we can
> keep second-guessing it, but either way my branch does not stand in its
> way. On the other hand, introducing bootresources.yaml in full detail into
> the user interface is committing publicly to the status quo.

Nail on head.

The user interface is the most important part. Underneath, this will start as
bootresources.yaml and silently migrate to some API calls when the work to fix
maas is done.

Revision history for this message
Julian Edwards (julian-edwards) :
review: Approve
157. By Jeroen T. Vermeulen

Test for coming change: pass user/host spec to scp as part of destination name. Tests: 2 failures.

158. By Jeroen T. Vermeulen

Satisfy tests.

159. By Jeroen T. Vermeulen

Merge trunk, resolve conflicts.

160. By Jeroen T. Vermeulen

Test for coming change: make bootresources.yamll world-readable. Tests: 1 failure.

161. By Jeroen T. Vermeulen

Satisfy test.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Turns out there were two small problems in the upload code (Raphaël, did you find these and patch around them in your Q/A run? It would have been useful to know!) but it's passing Q/A tests now. Landing.

For the record, the problems were all in the part that uploads bootresources.yaml — so all work that we would have had to do anyway even if we let users pass in a full bootresources.yaml file. The rest of the code has been no trouble at all.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'maastest/kvmfixture.py'
2--- maastest/kvmfixture.py 2014-04-09 06:19:08 +0000
3+++ maastest/kvmfixture.py 2014-04-10 06:19:43 +0000
4@@ -425,8 +425,9 @@
5 """
6 if dest is None:
7 dest = os.path.basename(source)
8+ remote_dest = self.identity() + ':' + dest
9 run_command(
10- ['scp'] + self._get_base_ssh_options() + [source, dest],
11+ ['scp'] + self._get_base_ssh_options() + [source, remote_dest],
12 check_call=True)
13
14 def get_ip_from_network_scan(self, mac_address):
15
16=== modified file 'maastest/maasfixture.py'
17--- maastest/maasfixture.py 2014-04-09 11:15:36 +0000
18+++ maastest/maasfixture.py 2014-04-10 06:19:43 +0000
19@@ -23,7 +23,9 @@
20 import logging
21 from random import choice
22 from string import ascii_letters
23+from tempfile import NamedTemporaryFile
24 from textwrap import dedent
25+import yaml
26
27 from apiclient.creds import convert_string_to_tuple
28 from apiclient.maas_client import (
29@@ -217,6 +219,43 @@
30 'metadataserver.NodeCommissionResult'
31 ])
32
33+ def make_selection(self, series, architecture):
34+ """Return a `selections` dict for the `bootresources.yaml` config."""
35+ arch, subarch = architecture.split('/')
36+ return {
37+ 'release': series,
38+ 'arches': [arch],
39+ 'subarches': [subarch],
40+ }
41+
42+ def compose_import_config(self, series, architectures):
43+ """Return a `bootresources.yaml` config file for the image imports."""
44+ return yaml.safe_dump({
45+ 'boot': {
46+ 'sources': [
47+ {
48+ 'selections': [
49+ self.make_selection(series, arch)
50+ for arch in architectures
51+ ],
52+ },
53+ ],
54+ },
55+ })
56+
57+ def upload_import_config(self, bootresources_yaml):
58+ """Upload `bootresources.yaml' into the VM's `/etc/maas/`."""
59+ with NamedTemporaryFile() as config:
60+ config.write(bootresources_yaml)
61+ config.flush()
62+ self.kvm_fixture.upload_file(config.name, 'bootresources.yaml')
63+ self.kvm_fixture.run_command(
64+ ['sudo', 'chmod', 'a+r', 'bootresources.yaml'],
65+ check_call=True)
66+ self.kvm_fixture.run_command(
67+ ['sudo', 'mv', 'bootresources.yaml', '/etc/maas/'],
68+ check_call=True)
69+
70 def import_maas_images(self, series, architecture,
71 simplestreams_filter=None):
72 """Import boot images into the MAAS instance.
73@@ -229,13 +268,11 @@
74 subarchitecture isn't specified, 'generic' is assumed.
75 """
76 arch_list = utils.mipf_arch_list(architecture)
77- arch_string = ' '.join(arch_list)
78 logging.info(
79 "Importing boot images series=%s, architectures=%s..." % (
80- series, arch_string))
81- # Import boot images.
82- # XXX jtv 2014-04-03: Configure /etc/maas/bootresources.yaml to import
83- # just the right series & architecture.
84+ series, ', '.join(arch_list)))
85+ self.upload_import_config(
86+ self.compose_import_config(self.series, arch_list))
87 self.kvm_fixture.run_command(
88 [
89 'sudo',
90
91=== modified file 'maastest/testing/factory.py'
92--- maastest/testing/factory.py 2014-04-09 06:19:53 +0000
93+++ maastest/testing/factory.py 2014-04-10 06:19:43 +0000
94@@ -13,11 +13,18 @@
95
96 __metaclass__ = type
97 __all__ = [
98+ 'make_arch_with_subarch',
99 'make_file',
100 ]
101
102 from fixtures import TempDir
103 import os.path
104+from random import randint
105+
106+
107+def make_arch_with_subarch():
108+ """Return an arbitrary `arch/subarch` string."""
109+ return 'arch-%d/subarch-%d' % (randint(1, 10000000), randint(1, 10000000))
110
111
112 def make_file(testcase, name=None, content=None):
113
114=== modified file 'maastest/tests/test_kvmfixture.py'
115--- maastest/tests/test_kvmfixture.py 2014-04-09 06:19:08 +0000
116+++ maastest/tests/test_kvmfixture.py 2014-04-10 06:19:43 +0000
117@@ -700,8 +700,11 @@
118 def test_upload_file_uploads_file(self):
119 self.patch(kvmfixture, 'run_command', mock.MagicMock())
120 fixture = self.make_KVMFixture(kvm_timeout=None)
121+ ip = '10.11.12.%d' % randint(1, 254)
122+ fixture.identity = lambda: 'ubuntu@%s' % ip
123 local_file = make_file(self)
124 remote_file = os.path.join(gettempdir(), self.getUniqueString())
125+ remote_path = 'ubuntu@%s:%s' % (ip, remote_file)
126 fixture.upload_file(local_file, remote_file)
127 self.assertEqual(
128 [
129@@ -709,7 +712,7 @@
130 (
131 ['scp'] +
132 fixture._get_base_ssh_options() +
133- [local_file, remote_file]
134+ [local_file, remote_path]
135 ),
136 check_call=True)
137 ],
138@@ -718,12 +721,15 @@
139 def test_upload_file_defaults_to_base_name_in_default_dir(self):
140 self.patch(kvmfixture, 'run_command', mock.MagicMock())
141 fixture = self.make_KVMFixture(kvm_timeout=None)
142+ fixture.identity = lambda: 'ubuntu@192.168.254.99'
143 local_file = make_file(self)
144 fixture.upload_file(local_file)
145 [command] = kvmfixture.run_command.mock_calls
146 name, args, kwargs = command
147 (command_line,) = args
148- self.assertEqual(os.path.basename(local_file), command_line[-1])
149+ self.assertEqual(
150+ fixture.identity() + ':' + os.path.basename(local_file),
151+ command_line[-1])
152
153
154 def xml_normalize(snippet):
155
156=== modified file 'maastest/tests/test_maasfixture.py'
157--- maastest/tests/test_maasfixture.py 2014-04-09 11:15:36 +0000
158+++ maastest/tests/test_maasfixture.py 2014-04-10 06:19:43 +0000
159@@ -22,7 +22,13 @@
160 import json
161 from random import randint
162 from subprocess import check_call
163+from testtools.matchers import (
164+ FileExists,
165+ HasLength,
166+ Not,
167+ )
168 from textwrap import dedent
169+import yaml
170
171 from apiclient.maas_client import MAASClient
172 from maastest import (
173@@ -32,7 +38,10 @@
174 )
175 from maastest.maas_enums import NODEGROUPINTERFACE_MANAGEMENT
176 from maastest.maasfixture import NoBootImagesError
177-from maastest.testing.factory import make_file
178+from maastest.testing.factory import (
179+ make_arch_with_subarch,
180+ make_file,
181+ )
182 import mock
183 import netaddr
184 from six import text_type
185@@ -68,11 +77,12 @@
186 'droopy', 'i860', proxy_url="http://example.com:8080")
187 fixture.run_command = mock.MagicMock(
188 return_value=(0, 'stdout output', 'stderr output'))
189+ fixture.upload_file = mock.MagicMock()
190 ip_address = '.'.join('%d' % randint(1, 254) for octets in range(4))
191 fixture.ip_address = mock.MagicMock(return_value=ip_address)
192 return fixture
193
194- def make_maas_fixture(self, kvm_fixture=None, proxy_url=None):
195+ def make_maas_fixture(self, kvm_fixture=None, proxy_url=None, arch=None):
196 """Create a `MAASFixture` with arbitrary series/architecture.
197
198 The maasfixture.MAASClient's post/get/put methods are replaced by a
199@@ -82,7 +92,8 @@
200 if kvm_fixture is None:
201 kvm_fixture = self.make_kvm_fixture()
202 series = self.getUniqueString()
203- arch = self.getUniqueString()
204+ if arch is None:
205+ arch = self.getUniqueString()
206 if proxy_url is None:
207 proxy_url = self.getUniqueString()
208 filter = self.getUniqueString()
209@@ -751,7 +762,7 @@
210 series = self.getUniqueString()
211 arch = self.getUniqueString()
212 filter = self.getUniqueString()
213- returned_arches = ['amd64', 'i386']
214+ returned_arches = ['amd64/generic', 'i386/generic']
215 mock_mipf_arch_list = mock.MagicMock(return_value=returned_arches)
216 self.patch(
217 utils, 'mipf_arch_list', mock_mipf_arch_list)
218@@ -781,6 +792,95 @@
219 'metadataserver.NodeCommissionResult']),
220 maas_fixture.kvm_fixture.run_command.mock_calls)
221
222+ def test_compose_import_config_uses_defaults(self):
223+ maas_fixture = self.make_maas_fixture()
224+ config = yaml.safe_load(
225+ maas_fixture.compose_import_config(
226+ maas_fixture.series, [make_arch_with_subarch()]))
227+ # This config only has the bare essentials. All other keys are left
228+ # out so that MAAS can provide defaults.
229+ self.assertItemsEqual(['boot'], config.keys())
230+ self.assertItemsEqual(['sources'], config['boot'].keys())
231+ self.assertThat(config['boot']['sources'], HasLength(1))
232+ [source] = config['boot']['sources']
233+ self.assertItemsEqual(['selections'], source.keys())
234+
235+ def test_compose_import_config_imports_requested_images(self):
236+ full_arch = make_arch_with_subarch()
237+ arch, subarch = full_arch.split('/')
238+ maas_fixture = self.make_maas_fixture(arch=full_arch)
239+ config = yaml.safe_load(maas_fixture.compose_import_config(
240+ maas_fixture.series, [full_arch]))
241+ [source] = config['boot']['sources']
242+ self.assertEqual(
243+ [{
244+ 'arches': [arch],
245+ 'subarches': [subarch],
246+ 'release': maas_fixture.series,
247+ }],
248+ source['selections'])
249+
250+ def test_compose_import_config_generates_one_selection_per_arch(self):
251+ arches = ['arch1/subarch1', 'arch2/subarch2']
252+ maas_fixture = self.make_maas_fixture()
253+ config = yaml.safe_load(maas_fixture.compose_import_config(
254+ maas_fixture.series, arches))
255+ [source] = config['boot']['sources']
256+ self.assertEqual(
257+ [
258+ {
259+ 'arches': ['arch1'],
260+ 'subarches': ['subarch1'],
261+ 'release': maas_fixture.series,
262+ },
263+ {
264+ 'arches': ['arch2'],
265+ 'subarches': ['subarch2'],
266+ 'release': maas_fixture.series,
267+ },
268+ ],
269+ source['selections'])
270+
271+ def test_upload_import_config_writes_config_to_temp_file(self):
272+
273+ class RecordingUpload:
274+ """Fake `upload_file` for testing.
275+
276+ Records the given files path, and its contents.
277+ """
278+ def __call__(self, local_file, remote_file=None):
279+ self.path = local_file
280+ with open(self.path, 'rb') as open_file:
281+ self.content = open_file.read()
282+
283+ maas_fixture = self.make_maas_fixture()
284+ fake_upload = RecordingUpload()
285+ maas_fixture.kvm_fixture.upload_file = fake_upload
286+ fake_config = self.getUniqueString()
287+ maas_fixture.upload_import_config(fake_config)
288+
289+ # The uploaded file was temporary; it no longer exists.
290+ self.assertThat(fake_upload.path, Not(FileExists()))
291+ # But its contents were as given.
292+ self.assertEqual(fake_config, fake_upload.content)
293+
294+ def test_upload_import_config_uploads_bootresources_config(self):
295+ maas_fixture = self.make_maas_fixture()
296+ maas_fixture.upload_import_config(self.getUniqueString())
297+ self.assertEqual(
298+ [mock.call(mock.ANY, 'bootresources.yaml')],
299+ maas_fixture.kvm_fixture.upload_file.mock_calls)
300+ self.assertEqual(
301+ [
302+ mock.call(
303+ ['sudo', 'chmod', 'a+r', 'bootresources.yaml'],
304+ check_call=True),
305+ mock.call(
306+ ['sudo', 'mv', 'bootresources.yaml', '/etc/maas/'],
307+ check_call=True),
308+ ],
309+ maas_fixture.kvm_fixture.run_command.mock_calls)
310+
311 def test_wait_until_boot_images_scanned_times_out_eventually(self):
312 maas_fixture = self.make_maas_fixture()
313 self.patch(
314
315=== modified file 'maastest/tests/test_utils.py'
316--- maastest/tests/test_utils.py 2014-04-04 17:05:59 +0000
317+++ maastest/tests/test_utils.py 2014-04-10 06:19:43 +0000
318@@ -35,7 +35,7 @@
319 )
320
321
322-class TestRunCommand(testtools.TestCase):
323+class TestReadFile(testtools.TestCase):
324
325 def test_read_file_returns_file_contents_as_bytes(self):
326 temp_dir = self.useFixture(TempDir())
327@@ -49,6 +49,9 @@
328 self.assertEqual(contents, read_contents)
329 self.assertIsInstance(contents, bytes)
330
331+
332+class TestRunCommand(testtools.TestCase):
333+
334 def test_run_command_calls_Popen(self):
335 mock_Popen = mock.MagicMock()
336 expected_retcode = self.getUniqueInteger()
337
338=== modified file 'maastest/utils.py'
339--- maastest/utils.py 2014-04-08 06:49:23 +0000
340+++ maastest/utils.py 2014-04-10 06:19:43 +0000
341@@ -13,15 +13,16 @@
342 __all__ = [
343 "BINARY",
344 "binary_content",
345+ "CasesLoader",
346+ "DEFAULT_PIDFILE_DIR",
347 "DEFAULT_STATE_DIR",
348+ "determine_vm_architecture",
349 "determine_vm_series",
350- "determine_vm_architecture",
351 "get_uri",
352- "DEFAULT_PIDFILE_DIR",
353+ "mipf_arch_list",
354 "read_file",
355 "retries",
356 "run_command",
357- "CasesLoader",
358 ]
359
360

Subscribers

People subscribed via source and target branches