summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-08-24 15:00:15 +0000
committerGerrit Code Review <review@openstack.org>2016-08-24 15:00:15 +0000
commita69a45f15c7a62672774c680af48e87580c954ee (patch)
treed2c7cc7bc970e8f1e9f49632dde866e697805187
parent9dbbee8fe1ed7a66e9b6513bda8a0911b02246da (diff)
parent51693817efb56d86cb5188f984e6ee952af0a55a (diff)
Merge "Add test for full CiCd flow"
-rw-r--r--test-requirements.txt2
-rwxr-xr-xtests/base.py234
-rw-r--r--tests/test_cicd_apps_flow.py333
-rw-r--r--tox.ini4
4 files changed, 553 insertions, 20 deletions
diff --git a/test-requirements.txt b/test-requirements.txt
index 8daf1d7..e7fb3ea 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -6,3 +6,5 @@ python-muranoclient
6python-heatclient 6python-heatclient
7python-novaclient 7python-novaclient
8 8
9python-jenkins
10git-review
diff --git a/tests/base.py b/tests/base.py
index dc32868..2041840 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -16,11 +16,13 @@
16import json 16import json
17import logging 17import logging
18import os 18import os
19import shutil
20import socket 19import socket
20import shutil
21import time 21import time
22import uuid 22import uuid
23from xml.etree import ElementTree as et
23 24
25import jenkins
24import paramiko 26import paramiko
25import requests 27import requests
26import testtools 28import testtools
@@ -75,8 +77,8 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
75 77
76 # Since its really useful to debug deployment after it fail lets 78 # Since its really useful to debug deployment after it fail lets
77 # add such possibility 79 # add such possibility
78 self.os_cleanup_before = str2bool('OS_CLEANUP_BEFORE', False) 80 self.os_cleanup_before = str2bool('OS_CLEANUP_BEFORE', True)
79 self.os_cleanup_after = str2bool('OS_CLEANUP_AFTER', True) 81 self.os_cleanup_after = str2bool('OS_CLEANUP_AFTER', False)
80 82
81 # Data for Nodepool app 83 # Data for Nodepool app
82 self.os_np_username = os.environ.get('OS_NP_USERNAME', self.os_username) 84 self.os_np_username = os.environ.get('OS_NP_USERNAME', self.os_username)
@@ -100,8 +102,9 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
100 # Application instance parameters 102 # Application instance parameters
101 self.flavor = os.environ.get('OS_FLAVOR', 'm1.medium') 103 self.flavor = os.environ.get('OS_FLAVOR', 'm1.medium')
102 self.image = os.environ.get('OS_IMAGE') 104 self.image = os.environ.get('OS_IMAGE')
105 self.docker_image = os.environ.get('OS_DOCKER_IMAGE')
103 self.files = [] 106 self.files = []
104 self.keyname, self.key_file = self._create_keypair() 107 self.keyname, self.pr_key, self.pub_key = self._create_keypair()
105 self.availability_zone = os.environ.get('OS_ZONE', 'nova') 108 self.availability_zone = os.environ.get('OS_ZONE', 'nova')
106 109
107 self.envs = [] 110 self.envs = []
@@ -163,8 +166,14 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
163 pr_key_file = self.create_file( 166 pr_key_file = self.create_file(
164 'id_{}'.format(kp_name), keypair.private_key 167 'id_{}'.format(kp_name), keypair.private_key
165 ) 168 )
166 self.create_file('id_{}.pub'.format(kp_name), keypair.public_key) 169 # Note: by default, permissions of created file with
167 return kp_name, pr_key_file 170 # private keypair is too open
171 os.chmod(pr_key_file, 0600)
172
173 pub_key_file = self.create_file(
174 'id_{}.pub'.format(kp_name), keypair.public_key
175 )
176 return kp_name, pr_key_file, pub_key_file
168 177
169 def _get_stack(self, environment_id): 178 def _get_stack(self, environment_id):
170 for stack in self.heat.stacks.list(): 179 for stack in self.heat.stacks.list():
@@ -269,7 +278,7 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
269 try: 278 try:
270 ssh = paramiko.SSHClient() 279 ssh = paramiko.SSHClient()
271 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 280 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
272 ssh.connect(fip, username='ubuntu', key_filename=self.key_file) 281 ssh.connect(fip, username='ubuntu', key_filename=self.pr_key)
273 ftp = ssh.open_sftp() 282 ftp = ssh.open_sftp()
274 ftp.get( 283 ftp.get(
275 '/var/log/murano-agent.log', 284 '/var/log/murano-agent.log',
@@ -368,22 +377,17 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
368 self.fail('{} port is not opened on instance'.format(port)) 377 self.fail('{} port is not opened on instance'.format(port))
369 378
370 def check_url_access(self, ip, path, port): 379 def check_url_access(self, ip, path, port):
371 attempt = 0
372 proto = 'http' if port not in (443, 8443) else 'https' 380 proto = 'http' if port not in (443, 8443) else 'https'
373 url = '%s://%s:%s/%s' % (proto, ip, port, path) 381 url = '{proto}://{ip}:{port}/{path}'.format(
382 proto=proto,
383 ip=ip,
384 port=port,
385 path=path
386 )
374 387
375 while attempt < 5: 388 resp = requests.get(url, timeout=60)
376 resp = requests.get(url)
377 if resp.status_code == 200:
378 LOG.debug('Service path "{}" is available'.format(url))
379 return
380 else:
381 time.sleep(5)
382 attempt += 1
383 389
384 self.fail( 390 return resp.status_code
385 'Service path {0} is unavailable after 5 attempts'.format(url)
386 )
387 391
388 def deployment_success_check(self, environment, services_map): 392 def deployment_success_check(self, environment, services_map):
389 deployment = self.murano.deployments.list(environment.id)[-1] 393 deployment = self.murano.deployments.list(environment.id)[-1]
@@ -414,3 +418,193 @@ class MuranoTestsBase(testtools.TestCase, clients.ClientsBase):
414 services_map[service]['url'], 418 services_map[service]['url'],
415 services_map[service]['url_port'] 419 services_map[service]['url_port']
416 ) 420 )
421
422 @staticmethod
423 def add_to_file(path_to_file, context):
424 with open(path_to_file, "a") as f:
425 f.write(context)
426
427 @staticmethod
428 def read_from_file(path_to_file):
429 with open(path_to_file, "r") as f:
430 return f.read()
431
432 def execute_cmd_on_remote_host(self, host, cmd, key_file, user='ubuntu'):
433 client = paramiko.SSHClient()
434 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
435 client.connect(hostname=host, username=user, key_filename=key_file)
436 stdin, stdout, stderr = client.exec_command(cmd)
437 data = stdout.read() + stderr.read()
438 client.close()
439
440 return data
441
442 def export_ssh_options(self):
443 context = (
444 '#!/bin/bash\n'
445 'ssh -o StrictHostKeyChecking=no -i {0} "$@"'.format(self.pr_key)
446 )
447 gitwrap = self.create_file('/tmp/gitwrap.sh', context)
448 os.chmod(gitwrap, 0744)
449 os.environ['GIT_SSH'] = gitwrap
450
451 def clone_repo(self, gerrit_host, gerrit_user, repo, dest_dir):
452 repo_dest_path = os.path.join(dest_dir, repo.split('/')[-1])
453 self.files.append(repo_dest_path)
454
455 if os.path.isdir(repo_dest_path):
456 shutil.rmtree(repo_dest_path)
457
458 os.system('git clone ssh://{user}@{host}:29418/{repo} {dest}'.format(
459 user=gerrit_user,
460 host=gerrit_host,
461 repo=repo,
462 dest=repo_dest_path
463 ))
464 return repo_dest_path
465
466 def switch_to_branch(self, repo, branch):
467 os.system('cd {repo}; git checkout {branch}'.format(
468 repo=repo,
469 branch=branch)
470 )
471
472 def add_committer_info(self, configfile, user, email):
473 author_data = """
474 [user]
475 name={0}
476 email={1}
477 """.format(user, email)
478 self.add_to_file(configfile, author_data)
479
480 def make_commit(self, repo, branch, key, msg):
481 # NOTE need to think how to use GIT_SSH
482 os.system(
483 'cd {repo};'
484 'git add . ; git commit -am "{msg}"; '
485 'ssh-agent bash -c "ssh-add {key}; '
486 'git-review -r origin {branch}"'.format(
487 repo=repo,
488 msg=msg,
489 key=key,
490 branch=branch
491 )
492 )
493
494 @staticmethod
495 def _gerrit_cmd(gerrit_host, cmd):
496 return (
497 'sudo su -c "ssh -p 29418 -i '
498 '/home/gerrit2/review_site/etc/ssh_project_rsa_key '
499 'project-creator@{host} {cmd}" '
500 'gerrit2'.format(host=gerrit_host, cmd=cmd)
501 )
502
503 def get_last_open_patch(self, gerrit_ip, gerrit_host, project, commit_msg):
504 cmd = (
505 'gerrit query --format JSON status:open '
506 'project:{project} limit:1'.format(project=project)
507 )
508 cmd = self._gerrit_cmd(gerrit_host, cmd)
509
510 # Note: "gerrit query" returns results describing changes that
511 # match the input query.
512 # Here is an example of results for above query:
513 # {"project":"open-paas/project-config" ... "number":"1",
514 # {"type":"stats","rowCount":1,"runTimeMilliseconds":219, ...}
515 # Output has to be cut using "head -1", because json.loads can't
516 # decode multiple jsons
517
518 patch = self.execute_cmd_on_remote_host(
519 host=gerrit_ip,
520 key_file=self.pr_key,
521 cmd='{} | head -1'.format(cmd)
522 )
523 patch = json.loads(patch)
524 self.assertIn(commit_msg, patch['commitMessage'])
525
526 return patch['number']
527
528 def merge_commit(self, gerrit_ip, gerrit_host, project, commit_msg):
529 changeid = self.get_last_open_patch(
530 gerrit_ip=gerrit_ip,
531 gerrit_host=gerrit_host,
532 project=project,
533 commit_msg=commit_msg
534 )
535
536 cmd = (
537 'gerrit review --project {project} --verified +2 '
538 '--code-review +2 --label Workflow=+1 '
539 '--submit {id},1'.format(project=project, id=changeid)
540 )
541 cmd = self._gerrit_cmd(gerrit_host, cmd)
542
543 self.execute_cmd_on_remote_host(
544 host=gerrit_ip,
545 user='ubuntu',
546 key_file=self.pr_key,
547 cmd=cmd
548 )
549
550 def set_tomcat_ip(self, pom_file, ip):
551 et.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
552 tree = et.parse(pom_file)
553 new_url = 'http://{ip}:8080/manager/text'.format(ip=ip)
554 ns = {'ns': 'http://maven.apache.org/POM/4.0.0'}
555 for plugin in tree.findall('ns:build/ns:plugins/', ns):
556 plugin_id = plugin.find('ns:artifactId', ns).text
557 if plugin_id == 'tomcat7-maven-plugin':
558 plugin.find('ns:configuration/ns:url', ns).text = new_url
559
560 tree.write(pom_file)
561
562 def get_gerrit_projects(self, gerrit_ip, gerrit_host):
563 cmd = self._gerrit_cmd(gerrit_host, 'gerrit ls-projects')
564 return self.execute_cmd_on_remote_host(
565 host=gerrit_ip,
566 user='ubuntu',
567 key_file=self.pr_key,
568 cmd=cmd
569 )
570
571 def get_jenkins_jobs(self, ip):
572 server = jenkins.Jenkins('http://{0}:8080'.format(ip))
573
574 return [job['name'] for job in server.get_all_jobs()]
575
576 def wait_for(self, func, expected, debug_msg, fail_msg, timeout, **kwargs):
577 LOG.debug(debug_msg)
578 start_time = time.time()
579
580 current = func(**kwargs)
581
582 def check(exp, cur):
583 if isinstance(cur, list) or isinstance(cur, str):
584 return exp not in cur
585 else:
586 return exp != cur
587
588 while check(expected, current):
589 current = func(**kwargs)
590
591 if time.time() - start_time > timeout:
592 self.fail("Time is out. {0}".format(fail_msg))
593 time.sleep(30)
594 LOG.debug('Expected result has been achieved.')
595
596 def get_last_build_number(self, ip, user, password, job_name, build_type):
597 server = jenkins.Jenkins(
598 'http://{0}:8080'.format(ip),
599 username=user,
600 password=password
601 )
602 # If there are no builds of desired type get_job_info returns None and
603 # it is not possible to get number, in this case this function returns
604 # None too and it means that there are no builds yet
605
606 build = server.get_job_info(job_name)[build_type]
607 if build:
608 return build['number']
609 else:
610 return build \ No newline at end of file
diff --git a/tests/test_cicd_apps_flow.py b/tests/test_cicd_apps_flow.py
new file mode 100644
index 0000000..9f8dc17
--- /dev/null
+++ b/tests/test_cicd_apps_flow.py
@@ -0,0 +1,333 @@
1# Copyright (c) 2016 Mirantis Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import base
17
18
19class MuranoCiCdFlowTest(base.MuranoTestsBase):
20
21 def test_run_cicd_flow(self):
22 ldap_user = 'user'
23 ldap_password = 'P@ssw0rd'
24 ldap_user_email = 'email@example.com'
25
26 environment = self.create_env()
27 session = self.create_session(environment)
28
29 cicd_json = {
30 '?': {
31 '_{id}'.format(id=self.generate_id().hex): {'name': 'CI/CD'},
32 'id': str(self.generate_id()),
33 'type':
34 'org.openstack.ci_cd_pipeline_murano_app.CiCdEnvironment'
35 },
36 'assignFloatingIp': True,
37 'availabilityZone': self.availability_zone,
38 'flavor': self.flavor,
39 'image': self.image,
40 'instance_name': environment.name,
41 'keyname': self.keyname,
42 'ldapEmail': ldap_user_email,
43 'ldapPass': 'P@ssw0rd',
44 'ldapRootEmail': 'root@example.com',
45 'ldapRootPass': ldap_password,
46 'ldapRootUser': 'root',
47 'ldapUser': ldap_user,
48 'userSSH': self.read_from_file(self.pub_key),
49 'name': 'CI/CD',
50 }
51
52 self.create_service(environment, session, cicd_json)
53 self.deploy_env(environment, session)
54
55 session = self.create_session(environment)
56 docker_json = {
57 'instance': {
58 'name': self.rand_name('Docker'),
59 'assignFloatingIp': True,
60 'keyname': self.keyname,
61 'flavor': 'm1.large',
62 'image': self.docker_image,
63 'availabilityZone': self.availability_zone,
64 '?': {
65 'type': 'io.murano.resources.LinuxMuranoInstance',
66 'id': self.generate_id()
67 },
68 },
69 'name': 'DockerVM',
70 '?': {
71 '_{id}'.format(id=self.generate_id().hex): {
72 'name': 'Docker VM Service'
73 },
74 'type': 'com.mirantis.docker.DockerStandaloneHost',
75 'id': str(self.generate_id())
76 }
77 }
78
79 docker = self.create_service(environment, session, docker_json)
80
81 tomcat_json = {
82 'host': docker,
83 'image': 'tutum/tomcat',
84 'name': 'Tomcat',
85 'port': 8080,
86 'password': 'admin',
87 'publish': True,
88 '?': {
89 '_{id}'.format(id=self.generate_id().hex): {
90 'name': 'Docker Tomcat'
91 },
92 'type': 'com.example.docker.DockerTomcat',
93 'id': str(self.generate_id())
94 }
95 }
96 self.create_service(environment, session, tomcat_json)
97
98 self.deploy_env(environment, session)
99
100 environment = self.get_env(environment)
101
102 check_services = {
103 'org.openstack.ci_cd_pipeline_murano_app.Jenkins': {
104 'ports': [8080, 22],
105 'url': 'api/',
106 'url_port': 8080
107 },
108 'org.openstack.ci_cd_pipeline_murano_app.Gerrit': {
109 'ports': [8081, 22],
110 'url': '#/admin/projects/',
111 'url_port': 8081
112 },
113 'org.openstack.ci_cd_pipeline_murano_app.OpenLDAP': {
114 'ports': [389, 22],
115 'url': None
116 },
117 'com.mirantis.docker.DockerStandaloneHost': {
118 'ports': [8080, 22],
119 'url': None
120 }
121 }
122
123 self.deployment_success_check(environment, check_services)
124
125 fips = self.get_services_fips(environment)
126
127 # Get Gerrit ip and hostname
128
129 gerrit_ip = fips['org.openstack.ci_cd_pipeline_murano_app.Gerrit']
130 gerrit_hostname = self.execute_cmd_on_remote_host(
131 host=gerrit_ip,
132 cmd='hostname -f',
133 key_file=self.pr_key
134 )[:-1]
135
136 self.export_ssh_options()
137
138 # Clone "project-config" repository
139
140 project_config_location = self.clone_repo(
141 gerrit_host=gerrit_ip,
142 gerrit_user=ldap_user,
143 repo='open-paas/project-config',
144 dest_dir='/tmp'
145 )
146
147 # Add new project to gerrit/projects.yaml
148
149 new_project = (
150 '- project: demo/petclinic\n'
151 ' description: petclinic new project\n'
152 ' upstream: https://github.com/sn00p/spring-petclinic\n'
153 ' acl-config: /home/gerrit2/acls/open-paas/project-config.config\n'
154 )
155 self.add_to_file(
156 '{0}/gerrit/projects.yaml'.format(project_config_location),
157 new_project
158 )
159
160 # Add committer info to project-config repo git config
161
162 self.add_committer_info(
163 configfile='{0}/.git/config'.format(project_config_location),
164 user=ldap_user,
165 email=ldap_user_email
166 )
167
168 # Make commit to project-config
169
170 self.make_commit(
171 repo=project_config_location,
172 branch='master',
173 key=self.pr_key,
174 msg='Add new project to gerrit/projects.yaml'
175 )
176
177 # Merge commit
178
179 self.merge_commit(
180 gerrit_ip=gerrit_ip,
181 gerrit_host=gerrit_hostname,
182 project='open-paas/project-config',
183 commit_msg='Add new project to gerrit/projects.yaml'
184 )
185
186 self.wait_for(
187 func=self.get_gerrit_projects,
188 expected='demo/petclinic',
189 debug_msg='Waiting while "demo/petlinic" project is created',
190 fail_msg='Project "demo/petclinic" wasn\'t created',
191 timeout=600,
192 gerrit_ip=gerrit_ip,
193 gerrit_host=gerrit_hostname,
194 )
195
196 # Create jenkins job for building petclinic app
197
198 new_job = (
199 '- project:\n'
200 ' name: petclinic\n'
201 ' jobs:\n'
202 ' - "{{name}}-java-app-deploy":\n'
203 ' git_url: "ssh://jenkins@{0}:29418/demo/petclinic"\n'
204 ' project: "demo/petclinic"\n'
205 ' branch: "Spring-Security"\n'
206 ' goals: tomcat7:deploy\n'.format(gerrit_hostname)
207 )
208 self.add_to_file(
209 '{0}/jenkins/jobs/projects.yaml'.format(project_config_location),
210 new_job
211 )
212
213 # Making commit to project-config
214
215 self.make_commit(
216 repo=project_config_location,
217 branch='master',
218 key=self.pr_key,
219 msg='Add job for petclinic app'
220 )
221
222 # Merge commit
223
224 self.merge_commit(
225 gerrit_ip=gerrit_ip,
226 gerrit_host=gerrit_hostname,
227 project='open-paas/project-config',
228 commit_msg='Add job for petclinic app'
229 )
230
231 # Wait while new "petclinic-java-app-deploy" job is created
232
233 self.wait_for(
234 func=self.get_jenkins_jobs,
235 expected='petclinic-java-app-deploy',
236 debug_msg='Waiting while "petclinic-java-app-deploy" is created',
237 fail_msg='Job "petclinic-java-app-deploy" wasn\'t created',
238 timeout=600,
239 ip=fips['org.openstack.ci_cd_pipeline_murano_app.Jenkins']
240 )
241
242 # Clone "demo/petclinic" repository
243
244 petclinic_location = self.clone_repo(
245 gerrit_host=gerrit_ip,
246 gerrit_user=ldap_user,
247 repo='demo/petclinic',
248 dest_dir='/tmp'
249 )
250
251 # Switch to "Spring-Security" branch
252
253 self.switch_to_branch(
254 repo=petclinic_location,
255 branch='Spring-Security'
256 )
257
258 # Set deployed Tomcat IP to pom.xml
259
260 self.set_tomcat_ip(
261 '{}/pom.xml'.format(petclinic_location),
262 fips['com.mirantis.docker.DockerStandaloneHost']
263 )
264
265 # Add committer info to demo/petclinic repo git config
266
267 self.add_committer_info(
268 configfile='{0}/.git/config'.format(petclinic_location),
269 user=ldap_user,
270 email=ldap_user_email
271 )
272
273 self.make_commit(
274 repo=petclinic_location,
275 branch='Spring-Security',
276 key=self.pr_key,
277 msg='Update Tomcat IP'
278 )
279
280 # Merge commit
281
282 self.merge_commit(
283 gerrit_ip=gerrit_ip,
284 gerrit_host=gerrit_hostname,
285 project='demo/petclinic',
286 commit_msg='Update Tomcat IP'
287 )
288
289 # Check that 'petclinic-java-app-deploy' (it triggers on-submit) was run
290
291 self.wait_for(
292 self.get_last_build_number,
293 expected=1,
294 debug_msg='Waiting while "petclinic-java-app-deploy" '
295 'job is run and first build is completed',
296 fail_msg='Job "petclinic-java-app-deploy" wasn\'t run on-submit',
297 timeout=900,
298 ip=fips['org.openstack.ci_cd_pipeline_murano_app.Jenkins'],
299 user=ldap_user,
300 password=ldap_password,
301 job_name='petclinic-java-app-deploy',
302 build_type='lastCompletedBuild'
303 )
304
305 # Check that 'petclinic-java-app-deploy' (it triggers on-submit) was
306 # finished and successful
307
308 self.wait_for(
309 self.get_last_build_number,
310 expected=1,
311 debug_msg='Checking that first build of "petclinic-java-app-deploy"'
312 ' job is successfully completed',
313 fail_msg='Job "petclinic-java-app-deploy" has failed',
314 timeout=60,
315 ip=fips['org.openstack.ci_cd_pipeline_murano_app.Jenkins'],
316 user=ldap_user,
317 password=ldap_password,
318 job_name='petclinic-java-app-deploy',
319 build_type='lastSuccessfulBuild'
320 )
321
322 # Check that Petclinic application was successfully deployed
323
324 self.wait_for(
325 func=self.check_url_access,
326 expected=200,
327 debug_msg='Checking that "petlinic" app is deployed and available',
328 fail_msg='Petclinic url isn\'t accessible.',
329 timeout=300,
330 ip=fips['com.mirantis.docker.DockerStandaloneHost'],
331 path='petclinic/',
332 port=8080
333 )
diff --git a/tox.ini b/tox.ini
index 3aeb0d3..dde079b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,6 +26,10 @@ commands = {posargs:}
26commands = python -m unittest tests.test_cicd_apps.MuranoCiCdTest.test_deploy_cicd 26commands = python -m unittest tests.test_cicd_apps.MuranoCiCdTest.test_deploy_cicd
27#commands = python setup.py testr --testr-args='{posargs}' 27#commands = python setup.py testr --testr-args='{posargs}'
28 28
29[testenv:run_cicd_flow]
30# FIXME!
31commands = python -m unittest tests.test_cicd_apps_flow.MuranoCiCdFlowTest.test_run_cicd_flow
32
29[testenv:hacking] 33[testenv:hacking]
30deps= 34deps=
31 ipdb 35 ipdb