Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/adding-cloud-target into lp:snappy-ecosystem-tests

Proposed by Heber Parrucci
Status: Merged
Approved by: Heber Parrucci
Approved revision: 57
Merged at revision: 57
Proposed branch: lp:~canonical-platform-qa/snappy-ecosystem-tests/adding-cloud-target
Merge into: lp:snappy-ecosystem-tests
Diff against target: 523 lines (+374/-31)
10 files modified
README.rst (+34/-0)
requirements-setup.txt (+1/-0)
requirements.txt (+0/-1)
run_setup (+50/-17)
snappy_ecosystem_tests/environment/cloud/__init__.py (+19/-0)
snappy_ecosystem_tests/environment/cloud/data/__init__.py (+19/-0)
snappy_ecosystem_tests/environment/cloud/data/deployment.py (+25/-0)
snappy_ecosystem_tests/environment/cloud/driver.py (+169/-0)
snappy_ecosystem_tests/environment/managers.py (+17/-0)
snappy_ecosystem_tests/environment/setup.py (+40/-13)
To merge this branch: bzr merge lp:~canonical-platform-qa/snappy-ecosystem-tests/adding-cloud-target
Reviewer Review Type Date Requested Status
platform-qa-bot continuous-integration Approve
Sergio Cazzolato (community) Needs Fixing
Snappy ecosystem tests developer Pending
Review via email: mp+320534@code.launchpad.net

Commit message

Adding cloud target for virtual machines provisioning using juju.

Description of the change

The change is for adding cloud as a new target.
The idea is to provision virtual machines using juju.
The new setup using cloud target will deploy a charm and by default install snapd and snapcraft in different virtual machines pointing to staging.
The corresponding driver was implemented using subprocess because python juju clients have several bugs and limitations. I tested these clients:
- jujuclient
- juju

For now it assumes that the juju controller/model is already bootstrapped in the machine where the setup is executed. But it will be also automated soon.

@run_tests: snappy_ecosystem_tests/tests/test_snapd.py

To post a comment you must log in.
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
49. By Heber Parrucci

Adding missing requirement: pyyaml

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote :

Some comments inline.
Also you should check the juju version and stops execution in case it is not >v2

review: Needs Fixing
50. By Heber Parrucci

Addressing review comments

Revision history for this message
Heber Parrucci (heber013) wrote :

Comments addressed. Added checking of juju >= 2

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
51. By Heber Parrucci

Adding cloud setup instructions on README.rst

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
52. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
53. By Heber Parrucci

* Removing specific revision to the charm url.
* Changing back default target to lxd.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
54. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
55. By Heber Parrucci

merge from trunk

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
56. By Heber Parrucci

increasing retries for juju deploy

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
57. By Heber Parrucci

adding proxy variable to juju deploy

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.rst'
2--- README.rst 2017-02-24 19:27:01 +0000
3+++ README.rst 2017-04-10 15:34:13 +0000
4@@ -125,3 +125,37 @@
5 $ bzr branch lp:snappy-ecosystem-tests
6 $ cd snappy-ecosystem-tests/
7 $ python3 -m snappy_ecosystem_tests.snapd.staging_builder my_new_container_name 16.10
8+
9+
10+How run the tests in openstack
11+==============================
12+
13+1- If you do not have a cloud/controller bootstrapped in your host, you can do it with a script in qakit:
14+
15+$ bzr branch lp:qakit; cd qakit
16+$ ./qakit/juju/juju_bootstrap --cloud_name ^THE_NAME_YOU_LIKE^ --nova_file ^NOVA_CREDENTIALS_FILE^
17+For example if you have canonistack credentials in $HOME/.canonistack/novarc:
18+$ ./qakit/juju/juju_bootstrap --cloud_name canonistack --nova_file "$HOME/.canonistack/novarc"
19+Enter the sudo password if asked (Some commands are executed as root and others do not).
20+Note: You need to provide the full path for the nova file. You can use $HOME but not ~.
21+It will install the needed packages and bootstrap the cloud.
22+The controller name will be the same of the cloud.
23+
24+Important note: You need to run the bootstrap only once per machine per cloud.
25+
26+2- Once the bootstrap finishes properly, you need to setup the snappy-ecosystem environment in the cloud,
27+For doing it, you should execute:
28+./run_setup --model ^controller_name:admin/default^ --target cloud
29+Note that if you do not pass the model, the default is: canonistack:admin/default
30+It will create by default 2 machines with applications and needed dependencies:
31+- snapcraft
32+- snapd
33+Both pointing to staging by default.
34+If you would like to specify the charm when running the setup, you can do it with --charm ^CHARM-URL^.
35+If not provided, it takes the default one.
36+
37+3- After setup finished properly. You should be able to run the tests:
38+./run_system_tests ^YOUR_TESTS^ --target cloud
39+
40+Note that the step 1 (juju bootstrap) needs to be executed only once per machine per cloud.
41+Then only will need to repeat the steps 2 and 3 when needed.
42
43=== modified file 'requirements-setup.txt'
44--- requirements-setup.txt 2017-03-06 18:06:27 +0000
45+++ requirements-setup.txt 2017-04-10 15:34:13 +0000
46@@ -1,2 +1,3 @@
47 pylxd
48 retrying
49+pyyaml
50
51=== modified file 'requirements.txt'
52--- requirements.txt 2017-04-03 16:05:21 +0000
53+++ requirements.txt 2017-04-10 15:34:13 +0000
54@@ -18,4 +18,3 @@
55 pyyaml
56 retrying
57 paramiko
58-#chromedriver_installer
59
60=== modified file 'run_setup'
61--- run_setup 2017-03-15 17:11:23 +0000
62+++ run_setup 2017-04-10 15:34:13 +0000
63@@ -28,41 +28,74 @@
64 proxy="$2"
65 shift # past argument
66 ;;
67- -m|--mode)
68- mode="$2"
69+ --target)
70+ target="$2"
71+ shift # past argument
72+ ;;
73+ --charm)
74+ charm="$2"
75+ shift # past argument
76+ ;;
77+ --model)
78+ model="$2"
79 shift # past argument
80 ;;
81 --profile)
82 profile="$2"
83 shift # past argument
84 ;;
85+ --series)
86+ series="$2"
87+ shift # past argument
88+ ;;
89+ --constraints)
90+ constraints="$2"
91+ shift # past argument
92+ ;;
93 esac
94 shift # past argument or value
95 done
96
97 . ./mk-venv -r requirements-setup.txt --proxy ${proxy}
98
99-if [ -z "$mode" ]; then
100- mode=lxd
101-fi
102-
103-if [ -z "$profile" ]; then
104- profile=staging
105-fi
106-
107-if [ -z "$proxy" ]; then
108- ve/bin/python3 -m snappy_ecosystem_tests.environment.setup --mode ${mode} --profile ${profile}
109-else
110- ve/bin/python3 -m snappy_ecosystem_tests.environment.setup --mode ${mode} --profile ${profile} --proxy ${proxy}
111-fi
112-
113+parameters='snappy_ecosystem_tests.environment.setup'
114+
115+if [ ! -z ${target+x} ]; then
116+ parameters="$parameters --target $target"
117+fi
118+
119+if [ ! -z ${proxy+x} ]; then
120+ parameters="$parameters --proxy $proxy"
121+fi
122+
123+if [ ! -z ${charm+x} ]; then
124+ parameters="$parameters --charm $charm"
125+fi
126+
127+if [ ! -z ${model+x} ]; then
128+ parameters="$parameters --model $model"
129+fi
130+
131+if [ ! -z ${profile+x} ]; then
132+ parameters="$parameters --profile $profile"
133+fi
134+
135+if [ ! -z ${series+x} ]; then
136+ parameters="$parameters --series $series"
137+fi
138+
139+if [ ! -z ${constraints+x} ]; then
140+ parameters="$parameters --constraints $constraints"
141+fi
142+
143+ve/bin/python3 -m ${parameters}
144
145 result=$?
146 deactivate
147
148 if [ $result != 0 ]; then
149 echo -e -n "Environment setup FAILED.\n"
150- exit 1;
151+ exit 1
152 else
153 echo -e -n "Environment setup PASSED.\n"
154 fi
155
156=== added directory 'snappy_ecosystem_tests/environment/cloud'
157=== added file 'snappy_ecosystem_tests/environment/cloud/__init__.py'
158--- snappy_ecosystem_tests/environment/cloud/__init__.py 1970-01-01 00:00:00 +0000
159+++ snappy_ecosystem_tests/environment/cloud/__init__.py 2017-04-10 15:34:13 +0000
160@@ -0,0 +1,19 @@
161+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
162+
163+#
164+# Snappy Ecosystem Tests
165+# Copyright (C) 2017 Canonical
166+#
167+# This program is free software: you can redistribute it and/or modify
168+# it under the terms of the GNU General Public License as published by
169+# the Free Software Foundation, either version 3 of the License, or
170+# (at your option) any later version.
171+#
172+# This program is distributed in the hope that it will be useful,
173+# but WITHOUT ANY WARRANTY; without even the implied warranty of
174+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
175+# GNU General Public License for more details.
176+#
177+# You should have received a copy of the GNU General Public License
178+# along with this program. If not, see <http://www.gnu.org/licenses/>.
179+#
180
181=== added directory 'snappy_ecosystem_tests/environment/cloud/data'
182=== added file 'snappy_ecosystem_tests/environment/cloud/data/__init__.py'
183--- snappy_ecosystem_tests/environment/cloud/data/__init__.py 1970-01-01 00:00:00 +0000
184+++ snappy_ecosystem_tests/environment/cloud/data/__init__.py 2017-04-10 15:34:13 +0000
185@@ -0,0 +1,19 @@
186+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
187+
188+#
189+# Snappy Ecosystem Tests
190+# Copyright (C) 2017 Canonical
191+#
192+# This program is free software: you can redistribute it and/or modify
193+# it under the terms of the GNU General Public License as published by
194+# the Free Software Foundation, either version 3 of the License, or
195+# (at your option) any later version.
196+#
197+# This program is distributed in the hope that it will be useful,
198+# but WITHOUT ANY WARRANTY; without even the implied warranty of
199+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
200+# GNU General Public License for more details.
201+#
202+# You should have received a copy of the GNU General Public License
203+# along with this program. If not, see <http://www.gnu.org/licenses/>.
204+#
205
206=== added file 'snappy_ecosystem_tests/environment/cloud/data/deployment.py'
207--- snappy_ecosystem_tests/environment/cloud/data/deployment.py 1970-01-01 00:00:00 +0000
208+++ snappy_ecosystem_tests/environment/cloud/data/deployment.py 2017-04-10 15:34:13 +0000
209@@ -0,0 +1,25 @@
210+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
211+
212+#
213+# Snappy Ecosystem Tests
214+# Copyright (C) 2017 Canonical
215+#
216+# This program is free software: you can redistribute it and/or modify
217+# it under the terms of the GNU General Public License as published by
218+# the Free Software Foundation, either version 3 of the License, or
219+# (at your option) any later version.
220+#
221+# This program is distributed in the hope that it will be useful,
222+# but WITHOUT ANY WARRANTY; without even the implied warranty of
223+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
224+# GNU General Public License for more details.
225+#
226+# You should have received a copy of the GNU General Public License
227+# along with this program. If not, see <http://www.gnu.org/licenses/>.
228+#
229+"""Contain information for the applications to be deployed on the cloud"""
230+
231+APPLICATIONS = [
232+ {'name': 'snapcraft', 'package': 'snapcraft', 'profile': 'staging'},
233+ {'name': 'snapd', 'package': 'snapd', 'profile': 'staging'},
234+]
235
236=== added file 'snappy_ecosystem_tests/environment/cloud/driver.py'
237--- snappy_ecosystem_tests/environment/cloud/driver.py 1970-01-01 00:00:00 +0000
238+++ snappy_ecosystem_tests/environment/cloud/driver.py 2017-04-10 15:34:13 +0000
239@@ -0,0 +1,169 @@
240+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
241+
242+#
243+# Snappy Ecosystem Tests
244+# Copyright (C) 2017 Canonical
245+#
246+# This program is free software: you can redistribute it and/or modify
247+# it under the terms of the GNU General Public License as published by
248+# the Free Software Foundation, either version 3 of the License, or
249+# (at your option) any later version.
250+#
251+# This program is distributed in the hope that it will be useful,
252+# but WITHOUT ANY WARRANTY; without even the implied warranty of
253+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
254+# GNU General Public License for more details.
255+#
256+# You should have received a copy of the GNU General Public License
257+# along with this program. If not, see <http://www.gnu.org/licenses/>.
258+#
259+
260+"""Module Drive virtual machines in the cloud using juju"""
261+
262+import subprocess
263+import tempfile
264+
265+import time
266+import yaml
267+from packaging import version
268+
269+
270+class CloudDriver:
271+
272+ """Class to Drive virtual machines in the cloud using juju"""
273+
274+ STATUSES_NOT_READY = ['maintenance', 'waiting']
275+
276+ def launch(self, charm, applications, profile, model, proxy=None, **kwargs):
277+ """Call to Deploy all the applications and then wait until all of them
278+ are deployed
279+
280+ :param charm: The charm to deploy
281+ :param applications: the applications to deploy
282+ :param profile: the profile of the application to deploy
283+ :param model: juju model in which the apps will be deployed
284+ :param proxy: the proxy URL.
285+ """
286+ assert self.get_version('juju') >= version.parse('2'), \
287+ 'Juju >= 2 is required.'
288+ subprocess.check_call(['juju', 'switch', model])
289+ for app in applications:
290+ self.deploy(charm, app, profile=profile, proxy=proxy, **kwargs)
291+ self.wait_until_deployed(applications)
292+
293+ @staticmethod
294+ def get_version(app):
295+ """Return the version of the given app"""
296+ app_version = subprocess.check_output(
297+ [app, 'version']).decode().split('-')[0]
298+ return version.parse(app_version)
299+
300+ def deploy(self, charm, app=None, profile=None, series=None,
301+ constraints=None, proxy=None):
302+ """
303+ Launch a machine deploying the given charm to the connected juju model.
304+
305+ :param charm: The charm to deploy
306+ :param app: the application name to deploy
307+ :param profile: the profile of the application to deploy
308+ :param series: series of the machine in which the application
309+ will be deployed.
310+ :param constraints: constraints for an application are set at deploy
311+ time.
312+ They must be expressed as a string containing only spaces
313+ and key value pairs joined by an '='. Example: mem=8G cores=4
314+ :param proxy: the proxy URL.
315+ """
316+ assert charm, 'You need to specify the charm to deploy'
317+ command = ['juju', 'deploy', charm]
318+ if series:
319+ command.extend(['--series', series])
320+ if constraints:
321+ command.extend(['--constraints', ' '.join(constraints)])
322+ if app:
323+ if self.get_application_status(app['name']):
324+ self.remove_application(app['name'])
325+ app_config = {app['name']: {'package': app['package'],
326+ 'profile': profile}}
327+ if proxy:
328+ app_config[app['name']]['proxy'] = proxy
329+ tmp_file = tempfile.NamedTemporaryFile()
330+ with open(tmp_file.name, 'w+') as temp:
331+ temp.write(yaml.dump(app_config))
332+ command.extend(['--config', tmp_file.name, app['name']])
333+ subprocess.check_call(command, universal_newlines=True)
334+
335+ def wait_until_deployed(self, apps, max_attempts=240, wait_period=5):
336+ """
337+ Wait until the given application is deployed.
338+
339+ :param apps: The applications list to check
340+ :param max_attempts: Amount of attempts to check before failing
341+ :param wait_period: The period of time in second to wait between checks
342+ :raise Exception: If the max attempts were reached
343+ """
344+ for app in apps:
345+ attempt = 0
346+ status = self.get_application_status(app['name'])
347+ while status in self.STATUSES_NOT_READY and attempt < max_attempts:
348+ time.sleep(wait_period)
349+ attempt += 1
350+ status = self.get_application_status(app['name'])
351+ if attempt == max_attempts:
352+ raise Exception(
353+ 'Deploy for application %s has failed' % app['name'])
354+
355+ @staticmethod
356+ def get_juju_status():
357+ """
358+ Get the status in dict format
359+
360+ :return: dict containing the juju status output
361+ """
362+ return yaml.load(subprocess.check_output(['juju', 'status',
363+ '--format', 'yaml'],
364+ universal_newlines=True))
365+
366+ def get_application_status(self, app_name):
367+ """Get the application status or None if it cannot be found"""
368+ status = self.get_juju_status()
369+ try:
370+ return status['applications'][app_name]['application-status'][
371+ 'current']
372+ except (KeyError, TypeError):
373+ return None
374+
375+ def remove_application(self, app_name, max_attempts=120, wait_period=5,
376+ wait=True):
377+ """Remove the given application
378+
379+ :param app_name: The application to remove
380+ :param max_attempts: Amount of attempts to check before failing
381+ :param wait_period: The period of time in second to wait between checks
382+ :param wait: whether to wait until the application is actually removed
383+ """
384+ try:
385+ subprocess.check_output(['juju', 'remove-application', app_name],
386+ universal_newlines=True,
387+ stderr=subprocess.STDOUT)
388+ except subprocess.CalledProcessError as _e:
389+ if 'not found' in _e.output:
390+ return
391+ else:
392+ raise
393+ attempt = 0
394+ if wait:
395+ status = self.get_application_status(app_name)
396+ while status and attempt < max_attempts:
397+ time.sleep(wait_period)
398+ attempt += 1
399+ status = self.get_application_status(app_name)
400+ if attempt == max_attempts:
401+ raise Exception('Deploy for application %s has failed' % app_name)
402+
403+ def get_ip(self, app_name):
404+ """Get the ip of the Machine in which the application is deployed"""
405+ status = self.get_juju_status()
406+ for key, value in status['applications'][app_name]['units'].items():
407+ if key.startswith(app_name):
408+ return value['public-address']
409
410=== modified file 'snappy_ecosystem_tests/environment/managers.py'
411--- snappy_ecosystem_tests/environment/managers.py 2017-03-27 20:21:15 +0000
412+++ snappy_ecosystem_tests/environment/managers.py 2017-04-10 15:34:13 +0000
413@@ -21,9 +21,15 @@
414
415 import logging
416
417+from snappy_ecosystem_tests.environment.cloud.driver import CloudDriver
418+
419 from snappy_ecosystem_tests.environment.constants import (
420 PROFILES, DEPENDENCIES)
421 from snappy_ecosystem_tests.environment.containers.lxd import LXDDriver
422+from snappy_ecosystem_tests.environment.cloud.data.deployment import (
423+ APPLICATIONS
424+)
425+
426
427 CONTAINERS = {
428 "snapd": {
429@@ -65,3 +71,14 @@
430 packages=[PROFILES[profile][cont.name]["package_name"]],
431 channel=PROFILES[profile][cont.name]["channel"])
432 self.driver.enable_ssh(cont)
433+
434+
435+class CloudManager:
436+ """Manage machines in the cloud"""
437+ def __init__(self):
438+ self.driver = CloudDriver()
439+
440+ def setup(self, profile, charm=None, model=None, proxy=None, **kwargs):
441+ """Setup virtual machines based on the profile"""
442+ self.driver.launch(charm, APPLICATIONS, profile, model,
443+ proxy=proxy, **kwargs)
444
445=== modified file 'snappy_ecosystem_tests/environment/setup.py'
446--- snappy_ecosystem_tests/environment/setup.py 2017-03-27 20:21:15 +0000
447+++ snappy_ecosystem_tests/environment/setup.py 2017-04-10 15:34:13 +0000
448@@ -22,35 +22,62 @@
449
450 import argparse
451
452-from snappy_ecosystem_tests.environment.managers import _LXDManager
453+from snappy_ecosystem_tests.environment.managers import (
454+ _LXDManager,
455+ CloudManager
456+)
457
458
459 SUPPORTED_MODULES = {
460- "lxd": _LXDManager
461+ "lxd": _LXDManager,
462+ "cloud": CloudManager,
463 }
464
465
466-def get_manager(mode):
467+def get_manager(target):
468 """Get the manager for the given mode"""
469 try:
470- return SUPPORTED_MODULES[mode]()
471+ return SUPPORTED_MODULES[target]()
472 except KeyError:
473- raise RuntimeError("Mode: {} is not supported".format(mode))
474-
475-
476-def main(mode, profile, proxy):
477+ raise RuntimeError("Target: {} is not supported".format(target))
478+
479+
480+def main(target, profile, **kwargs):
481 """Main script"""
482- manager = get_manager(mode)
483- manager.setup(profile, proxy)
484+ manager = get_manager(target)
485+ manager.setup(profile, **kwargs)
486
487
488 if __name__ == '__main__':
489 PARSER = argparse.ArgumentParser(description=
490 'Setup a snappy ecosystem environment.')
491- PARSER.add_argument('--mode', default="lxd", help='lxd')
492+ PARSER.add_argument('--target', default="lxd", help='lxd')
493 PARSER.add_argument('--profile', default="staging",
494 help='Profile to configure the environment')
495 PARSER.add_argument('--proxy', default=None,
496 help='Proxy to use in the target machine/s')
497- ARGS = PARSER.parse_args()
498- main(ARGS.mode, ARGS.profile, ARGS.proxy)
499+ PARSER.add_argument('--constraints', default=None, nargs='+',
500+ help='constraints for creating machine/s')
501+ ARGS, LEFT_OVER = PARSER.parse_known_args()
502+ if ARGS.target == 'cloud':
503+ CLOUD_PARSER = argparse.ArgumentParser(
504+ description='Parse cloud specific arguments')
505+ CLOUD_PARSER.add_argument('--charm',
506+ default='cs:~heber013/snappy-ecosystem',
507+ help='The juju charm to deploy')
508+ CLOUD_PARSER.add_argument('--model',
509+ default='canonistack:admin/default',
510+ help='The juju model in which apps will'
511+ ' be deployed')
512+ CLOUD_PARSER.add_argument('--series',
513+ help='The virtual machine series. '
514+ 'Example: xenial')
515+ CLOUD_ARGS, LEFT_OVER = CLOUD_PARSER.parse_known_args()
516+ main(ARGS.target, ARGS.profile,
517+ charm=CLOUD_ARGS.charm,
518+ model=CLOUD_ARGS.model,
519+ series=CLOUD_ARGS.series,
520+ constraints=ARGS.constraints,
521+ proxy=ARGS.proxy)
522+ else:
523+ main(ARGS.target, ARGS.profile, proxy=ARGS.proxy)

Subscribers

People subscribed via source and target branches