3 # Copyright (c) 2016 Rebaca and others.
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 """Juju testcase implementation."""
20 from copy import deepcopy
25 from snaps.config.flavor import FlavorConfig
26 from snaps.config.image import ImageConfig
27 from snaps.config.network import NetworkConfig, SubnetConfig
28 from snaps.config.router import RouterConfig
29 from snaps.config.security_group import (
30 Direction, Protocol, SecurityGroupConfig, SecurityGroupRuleConfig)
31 from snaps.config.user import UserConfig
32 from snaps.openstack.create_flavor import OpenStackFlavor
33 from snaps.openstack.create_image import OpenStackImage
34 from snaps.openstack.create_network import OpenStackNetwork
35 from snaps.openstack.create_router import OpenStackRouter
36 from snaps.openstack.create_security_group import OpenStackSecurityGroup
37 from snaps.openstack.create_user import OpenStackUser
38 from snaps.openstack.utils import keystone_utils
39 from snaps.openstack.utils import nova_utils
40 from snaps.openstack.utils import neutron_utils
42 from functest.core import vnf
43 from functest.opnfv_tests.openstack.snaps import snaps_utils
44 from functest.utils import config
45 from functest.utils import env
47 __author__ = "Amarendra Meher <amarendra@rebaca.com>"
48 __author__ = "Soumaya K Nayek <soumaya.nayek@rebaca.com>"
50 CLOUD_TEMPLATE = """clouds:
53 auth-types: [userpass]
59 CREDS_TEMPLATE2 = """credentials:
61 default-credential: abot-epc
65 project-domain-name: {project_domain_n}
66 tenant-name: {tenant_n}"""
68 CREDS_TEMPLATE3 = """credentials:
70 default-credential: abot-epc
74 project-domain-name: {project_domain_n}
75 tenant-name: {tenant_n}
76 user-domain-name: {user_domain_n}
80 class JujuEpc(vnf.VnfOnBoarding):
81 # pylint:disable=too-many-instance-attributes
82 """Abot EPC deployed with JUJU Orchestrator Case"""
84 __logger = logging.getLogger(__name__)
88 def __init__(self, **kwargs):
89 if "case_name" not in kwargs:
90 kwargs["case_name"] = "juju_epc"
91 super(JujuEpc, self).__init__(**kwargs)
93 # Retrieve the configuration
94 self.case_dir = pkg_resources.resource_filename(
95 'functest', 'opnfv_tests/vnf/epc')
97 self.config = getattr(
98 config.CONF, 'vnf_{}_config'.format(self.case_name))
100 raise Exception("VNF config file not found")
101 self.config_file = os.path.join(self.case_dir, self.config)
102 self.orchestrator = dict(requirements=get_config(
103 "orchestrator.requirements", self.config_file))
105 self.created_object = []
106 self.details['orchestrator'] = dict(
107 name=get_config("orchestrator.name", self.config_file),
108 version=get_config("orchestrator.version", self.config_file),
114 descriptor=get_config("vnf.descriptor", self.config_file),
115 requirements=get_config("vnf.requirements", self.config_file)
117 self.details['vnf'] = dict(
118 descriptor_version=self.vnf['descriptor']['version'],
119 name=get_config("vnf.name", self.config_file),
120 version=get_config("vnf.version", self.config_file),
122 self.__logger.debug("VNF configuration: %s", self.vnf)
124 self.details['test_vnf'] = dict(
125 name=get_config("vnf_test_suite.name", self.config_file),
126 version=get_config("vnf_test_suite.version", self.config_file),
127 tag_name=get_config("vnf_test_suite.tag_name", self.config_file)
129 self.public_auth_url = None
131 self.res_dir = os.path.join(
132 getattr(config.CONF, 'dir_results'), self.case_name)
134 def _bypass_juju_netdiscovery_bug(self, name):
135 user_creator = OpenStackUser(
139 password=str(uuid.uuid4()),
140 project_name=self.tenant_name,
141 domain_name=self.snaps_creds.user_domain_name,
142 roles={'_member_': self.tenant_name}))
143 user_creator.create()
144 self.created_object.append(user_creator)
147 def _register_cloud(self):
148 self.__logger.info("Creating Cloud for Abot-epc .....")
149 clouds_yaml = os.path.join(self.res_dir, "clouds.yaml")
151 'url': self.public_auth_url,
152 'region': self.snaps_creds.region_name}
153 with open(clouds_yaml, 'w') as yfile:
154 yfile.write(CLOUD_TEMPLATE.format(**cloud_data))
155 cmd = ['juju', 'add-cloud', 'abot-epc', '-f', clouds_yaml, '--replace']
156 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
157 self.__logger.info("%s\n%s", " ".join(cmd), output)
159 def _register_credentials_v2(self):
160 self.__logger.info("Creating Credentials for Abot-epc .....")
161 user_creator = self._bypass_juju_netdiscovery_bug(
162 'juju_network_discovery_bug')
163 snaps_creds = user_creator.get_os_creds(self.snaps_creds.project_name)
164 self.__logger.debug("snaps creds: %s", snaps_creds)
165 credentials_yaml = os.path.join(self.res_dir, "credentials.yaml")
167 'pass': snaps_creds.password,
168 'tenant_n': snaps_creds.project_name,
169 'user_n': snaps_creds.username}
170 with open(credentials_yaml, 'w') as yfile:
171 yfile.write(CREDS_TEMPLATE2.format(**creds_data))
172 cmd = ['juju', 'add-credential', 'abot-epc', '-f', credentials_yaml,
174 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
175 self.__logger.info("%s\n%s", " ".join(cmd), output)
177 def _register_credentials_v3(self):
178 self.__logger.info("Creating Credentials for Abot-epc .....")
179 user_creator = self._bypass_juju_netdiscovery_bug(
180 'juju_network_discovery_bug')
181 snaps_creds = user_creator.get_os_creds(self.snaps_creds.project_name)
182 self.__logger.debug("snaps creds: %s", snaps_creds)
183 credentials_yaml = os.path.join(self.res_dir, "credentials.yaml")
185 'pass': snaps_creds.password,
186 'tenant_n': snaps_creds.project_name,
187 'user_n': snaps_creds.username,
188 'project_domain_n': snaps_creds.project_domain_name,
189 'user_domain_n': snaps_creds.user_domain_name}
190 with open(credentials_yaml, 'w') as yfile:
191 yfile.write(CREDS_TEMPLATE3.format(**creds_data))
192 cmd = ['juju', 'add-credential', 'abot-epc', '-f', credentials_yaml,
194 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
195 self.__logger.info("%s\n%s", " ".join(cmd), output)
197 def _add_custom_rule(self, sec_grp_name):
198 """ To add custom rule for SCTP Traffic """
200 security_group = OpenStackSecurityGroup(
205 security_group.create()
207 # Add custom security rule to the obtained Security Group
208 self.__logger.info("Adding SCTP ingress rule to SG:%s",
209 security_group.sec_grp_settings.name)
212 security_group.add_rule(SecurityGroupRuleConfig(
213 sec_grp_name=sec_grp_name, direction=Direction.ingress,
214 protocol=Protocol.sctp))
215 except Exception: # pylint: disable=broad-except
216 self.__logger.exception(
217 "Some issue encountered with adding SCTP security rule ...")
220 """Prepare testcase (Additional pre-configuration steps)."""
221 self.__logger.info("Additional pre-configuration steps")
222 super(JujuEpc, self).prepare()
224 os.makedirs(self.res_dir)
225 except OSError as ex:
226 if ex.errno != errno.EEXIST:
227 self.__logger.exception("Cannot create %s", self.res_dir)
228 raise vnf.VnfPreparationException
230 self.__logger.info("ENV:\n%s", env.string())
232 self.public_auth_url = keystone_utils.get_endpoint(
233 self.snaps_creds, 'identity')
235 # it enforces a versioned public identity endpoint as juju simply
236 # adds /auth/tokens wich fails vs an unversioned endpoint.
237 if not self.public_auth_url.endswith(('v3', 'v3/', 'v2.0', 'v2.0/')):
238 self.public_auth_url = six.moves.urllib.parse.urljoin(
239 self.public_auth_url, 'v3')
240 self._register_cloud()
241 if self.snaps_creds.identity_api_version == 3:
242 self._register_credentials_v3()
244 self._register_credentials_v2()
246 def deploy_orchestrator(self): # pylint: disable=too-many-locals
248 Create network, subnet, router
252 self.__logger.info("Deploying Juju Orchestrator")
253 private_net_name = getattr(
254 config.CONF, 'vnf_{}_private_net_name'.format(self.case_name))
255 private_subnet_name = '{}-{}'.format(
257 'vnf_{}_private_subnet_name'.format(self.case_name)),
259 private_subnet_cidr = getattr(
260 config.CONF, 'vnf_{}_private_subnet_cidr'.format(self.case_name))
261 abot_router = '{}-{}'.format(
263 'vnf_{}_external_router'.format(self.case_name)),
265 self.__logger.info("Creating full network with nameserver: %s",
266 env.get('NAMESERVER'))
267 subnet_settings = SubnetConfig(
268 name=private_subnet_name,
269 cidr=private_subnet_cidr,
270 dns_nameservers=[env.get('NAMESERVER')])
271 network_settings = NetworkConfig(
272 name=private_net_name, subnet_settings=[subnet_settings])
273 network_creator = OpenStackNetwork(self.snaps_creds, network_settings)
274 net_id = network_creator.create().id
275 self.created_object.append(network_creator)
277 ext_net_name = snaps_utils.get_ext_net_name(self.snaps_creds)
278 self.__logger.info("Creating network Router ....")
279 router_creator = OpenStackRouter(
280 self.snaps_creds, RouterConfig(
282 external_gateway=ext_net_name,
283 internal_subnets=[subnet_settings.name]))
284 router_creator.create()
285 self.created_object.append(router_creator)
286 self.__logger.info("Creating Flavor ....")
287 flavor_settings = FlavorConfig(
288 name=self.orchestrator['requirements']['flavor']['name'],
289 ram=self.orchestrator['requirements']['flavor']['ram_min'],
291 flavor_creator = OpenStackFlavor(self.snaps_creds, flavor_settings)
292 flavor_creator.create()
293 self.created_object.append(flavor_creator)
295 self.__logger.info("Upload some OS images if it doesn't exist")
296 images = get_config("tenant_images", self.config_file)
297 self.__logger.info("Images needed for vEPC: %s", images)
298 for image_name, image_file in six.iteritems(images):
299 self.__logger.info("image: %s, file: %s", image_name, image_file)
300 if image_file and image_name:
301 image_creator = OpenStackImage(self.snaps_creds, ImageConfig(
302 name=image_name, image_user='cloud', img_format='qcow2',
303 image_file=image_file))
304 image_id = image_creator.create().id
305 cmd = ['juju', 'metadata', 'generate-image', '-d', '/root',
306 '-i', image_id, '-s', image_name,
307 '-r', self.snaps_creds.region_name,
308 '-u', self.public_auth_url]
309 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
310 self.__logger.info("%s\n%s", " ".join(cmd), output)
311 self.created_object.append(image_creator)
312 self.__logger.info("Network ID : %s", net_id)
314 self.__logger.info("Starting Juju Bootstrap process...")
316 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
317 'juju', 'bootstrap', 'abot-epc', 'abot-controller',
318 '--metadata-source', '/root',
319 '--constraints', 'mem=2G',
320 '--bootstrap-series', 'xenial',
321 '--config', 'network={}'.format(net_id),
322 '--config', 'ssl-hostname-verification=false',
323 '--config', 'use-floating-ip=true',
324 '--config', 'use-default-secgroup=true',
326 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
327 self.__logger.info("%s\n%s", " ".join(cmd), output)
328 except subprocess.CalledProcessError as cpe:
330 "Exception with Juju Bootstrap: %s\n%s",
333 except Exception: # pylint: disable=broad-except
334 self.__logger.exception("Some issue with Juju Bootstrap ...")
339 def check_app(self, name='abot-epc-basic', status='active'):
340 """Check application status."""
341 cmd = ['juju', 'status', '--format', 'short', name]
342 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
343 self.__logger.info("%s\n%s", " ".join(cmd), output)
344 ret = re.search(r'(?=workload:({})\))'.format(status), output)
346 self.__logger.info("%s workload is %s", name, status)
348 self.__logger.error("%s workload differs from %s", name, status)
351 def deploy_vnf(self):
352 """Deploy ABOT-OAI-EPC."""
353 self.__logger.info("Upload VNFD")
354 descriptor = self.vnf['descriptor']
355 self.__logger.info("Get or create flavor for all Abot-EPC")
356 flavor_settings = FlavorConfig(
357 name=self.vnf['requirements']['flavor']['name'],
358 ram=self.vnf['requirements']['flavor']['ram_min'],
361 flavor_creator = OpenStackFlavor(self.snaps_creds, flavor_settings)
362 flavor_creator.create()
363 self.created_object.append(flavor_creator)
365 self.__logger.info("Deploying Abot-epc bundle file ...")
366 cmd = ['juju', 'deploy', '{}'.format(descriptor.get('file_name'))]
367 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
368 self.__logger.info("%s\n%s", " ".join(cmd), output)
369 self.__logger.info("Waiting for instances .....")
371 cmd = ['timeout', '-t', JujuEpc.juju_timeout, 'juju-wait']
372 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
373 self.__logger.info("%s\n%s", " ".join(cmd), output)
374 self.__logger.info("Deployed Abot-epc on Openstack")
375 except subprocess.CalledProcessError as cpe:
377 "Exception with Juju VNF Deployment: %s\n%s",
380 except Exception: # pylint: disable=broad-except
381 self.__logger.exception("Some issue with the VNF Deployment ..")
384 self.__logger.info("Checking status of ABot and EPC units ...")
385 cmd = ['juju', 'status']
386 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
387 self.__logger.debug("%s\n%s", " ".join(cmd), output)
388 for app in ['abot-epc-basic', 'oai-epc', 'oai-hss']:
389 if not self.check_app(app):
392 nova_client = nova_utils.nova_client(self.snaps_creds)
393 instances = get_instances(nova_client)
394 self.__logger.info("List of Instance: %s", instances)
395 for items in instances:
396 metadata = get_instance_metadata(nova_client, items)
397 if 'juju-units-deployed' in metadata:
398 sec_group = 'juju-{}-{}'.format(
399 metadata['juju-controller-uuid'],
400 metadata['juju-model-uuid'])
401 self.__logger.info("Instance: %s", sec_group)
403 self.__logger.info("Adding Security group rule....")
404 # This will add sctp rule to a common Security Group Created
405 # by juju and shared to all deployed units.
406 self._add_custom_rule(sec_group)
408 self.__logger.info("Transferring the feature files to Abot_node ...")
409 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
410 'juju', 'scp', '--', '-r', '-v',
411 '{}/featureFiles'.format(self.case_dir), 'abot-epc-basic/0:~/']
412 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
413 self.__logger.info("%s\n%s", " ".join(cmd), output)
415 self.__logger.info("Copying the feature files within Abot_node ")
416 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
417 'juju', 'ssh', 'abot-epc-basic/0',
418 'sudo', 'cp', '-vfR', '~/featureFiles/*',
419 '/etc/rebaca-test-suite/featureFiles']
420 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
421 self.__logger.info("%s\n%s", " ".join(cmd), output)
425 """Run test on ABoT."""
426 start_time = time.time()
427 self.__logger.info("Running VNF Test cases....")
428 cmd = ['juju', 'run-action', 'abot-epc-basic/0', 'run',
429 'tagnames={}'.format(self.details['test_vnf']['tag_name'])]
430 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
431 self.__logger.info("%s\n%s", " ".join(cmd), output)
433 cmd = ['timeout', '-t', JujuEpc.juju_timeout, 'juju-wait']
434 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
435 self.__logger.info("%s\n%s", " ".join(cmd), output)
437 duration = time.time() - start_time
438 self.__logger.info("Getting results from Abot node....")
439 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
440 'juju', 'scp', '--', '-v',
442 '/var/lib/abot-epc-basic/artifacts/TestResults.json',
443 '{}/.'.format(self.res_dir)]
444 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
445 self.__logger.info("%s\n%s", " ".join(cmd), output)
446 self.__logger.info("Parsing the Test results...")
447 res = (process_abot_test_result('{}/TestResults.json'.format(
449 short_result = sig_test_format(res)
450 self.__logger.info(short_result)
451 self.details['test_vnf'].update(
452 status='PASS', result=short_result, full_result=res,
455 "Test VNF result: Passed: %d, Failed:%d, Skipped: %d",
456 short_result['passed'],
457 short_result['failures'], short_result['skipped'])
460 def _get_floating_ips(self):
461 """Get the list of floating IPs associated with the current project"""
463 project_id = self.os_project.get_project().id
465 neutron_client = neutron_utils.neutron_client(self.snaps_creds)
466 floating_ips = neutron_utils.get_floating_ips(neutron_client)
468 project_floating_ip_list = list()
469 for floating_ip in floating_ips:
470 if project_id and project_id == floating_ip.project_id:
471 project_floating_ip_list.append(floating_ip)
473 return project_floating_ip_list
475 def _release_floating_ips(self, fip_list):
477 Responsible for deleting a list of floating IPs
478 :param fip_list: A list of SNAPS FloatingIp objects
484 neutron_client = neutron_utils.neutron_client(self.snaps_creds)
486 for floating_ip in fip_list:
487 neutron_utils.delete_floating_ip(neutron_client, floating_ip)
490 """Clean created objects/functions."""
492 # Store Floating IPs of instances created by Juju
493 fip_list = self._get_floating_ips()
494 self.__logger.info("Floating IPs assigned to project:%s",
495 self.os_project.get_project().name)
496 for floating_ip in fip_list:
497 self.__logger.debug("%s:%s", floating_ip.ip,
498 floating_ip.description)
501 cmd = ['juju', 'debug-log', '--replay', '--no-tail']
502 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
503 self.__logger.debug("%s\n%s", " ".join(cmd), output)
504 if not self.orchestrator['requirements']['preserve_setup']:
505 self.__logger.info("Destroying Orchestrator...")
506 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
507 'juju', 'destroy-controller', '-y', 'abot-controller',
508 '--destroy-all-models']
509 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
510 self.__logger.info("%s\n%s", " ".join(cmd), output)
511 except subprocess.CalledProcessError as cpe:
513 "Exception with Juju Cleanup: %s\n%s",
515 except Exception: # pylint: disable=broad-except
516 self.__logger.exception("General issue during the undeployment ..")
518 if not self.orchestrator['requirements']['preserve_setup']:
520 self.__logger.info('Release floating IPs assigned by Juju...')
521 self._release_floating_ips(fip_list)
522 except Exception: # pylint: disable=broad-except
523 self.__logger.exception(
524 "Exception while releasing floating IPs ...")
526 self.__logger.info('Remove the Abot_epc OS objects ..')
527 super(JujuEpc, self).clean()
532 # ----------------------------------------------------------
536 # -----------------------------------------------------------
537 def get_config(parameter, file_path):
539 Returns the value of a given parameter in file.yaml
540 parameter must be given in string format with dots
541 Example: general.openstack.image_name
543 with open(file_path) as config_file:
544 file_yaml = yaml.safe_load(config_file)
547 for element in parameter.split("."):
548 value = value.get(element)
550 raise ValueError("The parameter %s is not defined in"
551 " reporting.yaml" % parameter)
555 def sig_test_format(sig_test):
557 Process the signaling result to have a short result
562 for data_test in sig_test:
563 if data_test['result'] == "passed":
565 elif data_test['result'] == "failed":
567 elif data_test['result'] == "skipped":
569 total_sig_test_result = {}
570 total_sig_test_result['passed'] = nb_passed
571 total_sig_test_result['failures'] = nb_failures
572 total_sig_test_result['skipped'] = nb_skipped
573 return total_sig_test_result
576 def process_abot_test_result(file_path):
577 """ Process ABoT Result """
578 with open(file_path) as test_result:
579 data = json.load(test_result)
582 tests = update_data(tests)
584 flatten_steps = tests['elements'][0].pop('flatten_steps')
585 for steps in flatten_steps:
586 steps['result'] = steps['step_status']
588 except Exception: # pylint: disable=broad-except
589 logging.error("Could not post data to ElasticSearch host")
594 def update_data(obj):
595 """ Update Result data"""
597 obj['feature_file'] = os.path.splitext(os.path.basename(obj['uri']))[0]
599 for element in obj['elements']:
600 element['final_result'] = "passed"
601 element['flatten_steps'] = []
603 for step in element['steps']:
605 step['result'][step['result']['status']] = 1
606 if step['result']['status'].lower() in ['fail', 'failed']:
607 element['final_result'] = "failed"
609 temp_dict['feature_file'] = obj['feature_file']
610 temp_dict['step_name'] = step['name']
611 temp_dict['step_status'] = step['result']['status']
612 temp_dict['step_duration'] = step['result'].get('duration', 0)
613 temp_dict['step_' + step['result']['status']] = 1
614 element['flatten_steps'].append(deepcopy(temp_dict))
616 # Need to put the tag in OBJ and not ELEMENT
618 element['tags'] = deepcopy(obj['tags'])
619 for tag in obj['tags']:
620 element[tag['name']] = 1
622 for tag in element['tags']:
623 element[tag['name']] = 1
625 except Exception: # pylint: disable=broad-except
626 logging.error("Error in updating data, %s", sys.exc_info()[0])
632 def get_instances(nova_client):
633 """ To get all vm info of a project """
635 instances = nova_client.servers.list()
637 except Exception as exc: # pylint: disable=broad-except
638 logging.error("Error [get_instances(nova_client)]: %s", exc)
642 def get_instance_metadata(nova_client, instance):
643 """ Get instance Metadata - Instance ID """
645 instance = nova_client.servers.get(instance.id)
646 return instance.metadata
647 except Exception as exc: # pylint: disable=broad-except
648 logging.error("Error [get_instance_status(nova_client)]: %s", exc)