83f9778fc9d01bf301a8cdadccca05833dd34fca
[functest.git] / functest / opnfv_tests / vnf / epc / juju_epc.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2016 Rebaca and others.
4 #
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."""
10
11 import errno
12 import logging
13 import os
14 import time
15 import json
16 import re
17 import subprocess
18 import sys
19 import uuid
20 from copy import deepcopy
21 from urlparse import urljoin
22 import pkg_resources
23 import yaml
24
25 from functest.core import vnf
26 from functest.opnfv_tests.openstack.snaps import snaps_utils
27 from functest.utils import config
28 from functest.utils import env
29
30 from snaps.config.flavor import FlavorConfig
31 from snaps.config.image import ImageConfig
32 from snaps.config.network import NetworkConfig, SubnetConfig
33 from snaps.config.router import RouterConfig
34 from snaps.config.security_group import (
35     Direction, Protocol, SecurityGroupConfig, SecurityGroupRuleConfig)
36 from snaps.config.user import UserConfig
37 from snaps.openstack.create_flavor import OpenStackFlavor
38 from snaps.openstack.create_image import OpenStackImage
39 from snaps.openstack.create_network import OpenStackNetwork
40 from snaps.openstack.create_router import OpenStackRouter
41 from snaps.openstack.create_security_group import OpenStackSecurityGroup
42 from snaps.openstack.create_user import OpenStackUser
43 from snaps.openstack.utils import keystone_utils
44 from snaps.openstack.utils import nova_utils
45
46 __author__ = "Amarendra Meher <amarendra@rebaca.com>"
47 __author__ = "Soumaya K Nayek <soumaya.nayek@rebaca.com>"
48
49 CLOUD_TEMPLATE = """clouds:
50   abot-epc:
51     type: openstack
52     auth-types: [userpass]
53     endpoint: {url}
54     regions:
55       {region}:
56         endpoint: {url}"""
57
58 CREDS_TEMPLATE2 = """credentials:
59   abot-epc:
60     default-credential: abot-epc
61     abot-epc:
62       auth-type: userpass
63       password: {pass}
64       project-domain-name: {project_domain_n}
65       tenant-name: {tenant_n}"""
66
67 CREDS_TEMPLATE3 = """credentials:
68   abot-epc:
69     default-credential: abot-epc
70     abot-epc:
71       auth-type: userpass
72       password: {pass}
73       project-domain-name: {project_domain_n}
74       tenant-name: {tenant_n}
75       user-domain-name: {user_domain_n}
76       username: {user_n}"""
77
78
79 class JujuEpc(vnf.VnfOnBoarding):
80     # pylint:disable=too-many-instance-attributes
81     """Abot EPC deployed with JUJU Orchestrator Case"""
82
83     __logger = logging.getLogger(__name__)
84
85     juju_timeout = '3600'
86
87     def __init__(self, **kwargs):
88         if "case_name" not in kwargs:
89             kwargs["case_name"] = "juju_epc"
90         super(JujuEpc, self).__init__(**kwargs)
91
92         # Retrieve the configuration
93         self.case_dir = pkg_resources.resource_filename(
94             'functest', 'opnfv_tests/vnf/epc')
95         try:
96             self.config = getattr(
97                 config.CONF, 'vnf_{}_config'.format(self.case_name))
98         except Exception:
99             raise Exception("VNF config file not found")
100         self.config_file = os.path.join(self.case_dir, self.config)
101         self.orchestrator = dict(requirements=get_config(
102             "orchestrator.requirements", self.config_file))
103
104         self.created_object = []
105         self.details['orchestrator'] = dict(
106             name=get_config("orchestrator.name", self.config_file),
107             version=get_config("orchestrator.version", self.config_file),
108             status='ERROR',
109             result=''
110         )
111
112         self.vnf = dict(
113             descriptor=get_config("vnf.descriptor", self.config_file),
114             requirements=get_config("vnf.requirements", self.config_file)
115         )
116         self.details['vnf'] = dict(
117             descriptor_version=self.vnf['descriptor']['version'],
118             name=get_config("vnf.name", self.config_file),
119             version=get_config("vnf.version", self.config_file),
120         )
121         self.__logger.debug("VNF configuration: %s", self.vnf)
122
123         self.details['test_vnf'] = dict(
124             name=get_config("vnf_test_suite.name", self.config_file),
125             version=get_config("vnf_test_suite.version", self.config_file),
126             tag_name=get_config("vnf_test_suite.tag_name", self.config_file)
127         )
128         self.public_auth_url = None
129
130         self.res_dir = os.path.join(
131             getattr(config.CONF, 'dir_results'), self.case_name)
132
133     def _bypass_juju_network_discovery_bug(self, name):
134         user_creator = OpenStackUser(
135             self.snaps_creds,
136             UserConfig(
137                 name=name,
138                 password=str(uuid.uuid4()),
139                 project_name=self.tenant_name,
140                 domain=self.snaps_creds.user_domain_name,
141                 roles={'_member_': self.tenant_name}))
142         user_creator.create()
143         self.created_object.append(user_creator)
144         return user_creator
145
146     def _register_cloud(self):
147         self.__logger.info("Creating Cloud for Abot-epc .....")
148         clouds_yaml = os.path.join(self.res_dir, "clouds.yaml")
149         cloud_data = {
150             'url': self.public_auth_url,
151             'region': self.snaps_creds.region_name}
152         with open(clouds_yaml, 'w') as yfile:
153             yfile.write(CLOUD_TEMPLATE.format(**cloud_data))
154         cmd = ['juju', 'add-cloud', 'abot-epc', '-f', clouds_yaml, '--replace']
155         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
156         self.__logger.info("%s\n%s", " ".join(cmd), output)
157
158     def _register_credentials_v2(self):
159         self.__logger.info("Creating Credentials for Abot-epc .....")
160         user_creator = self._bypass_juju_network_discovery_bug(
161             'juju_network_discovery_bug')
162         snaps_creds = user_creator.get_os_creds(self.snaps_creds.project_name)
163         self.__logger.debug("snaps creds: %s", snaps_creds)
164         credentials_yaml = os.path.join(self.res_dir, "credentials.yaml")
165         creds_data = {
166             'pass': snaps_creds.password,
167             'tenant_n': snaps_creds.project_name,
168             'user_n': snaps_creds.username}
169         with open(credentials_yaml, 'w') as yfile:
170             yfile.write(CREDS_TEMPLATE2.format(**creds_data))
171         cmd = ['juju', 'add-credential', 'abot-epc', '-f', credentials_yaml,
172                '--replace']
173         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
174         self.__logger.info("%s\n%s", " ".join(cmd), output)
175
176     def _register_credentials_v3(self):
177         self.__logger.info("Creating Credentials for Abot-epc .....")
178         user_creator = self._bypass_juju_network_discovery_bug(
179             'juju_network_discovery_bug')
180         snaps_creds = user_creator.get_os_creds(self.snaps_creds.project_name)
181         self.__logger.debug("snaps creds: %s", snaps_creds)
182         credentials_yaml = os.path.join(self.res_dir, "credentials.yaml")
183         creds_data = {
184             'pass': snaps_creds.password,
185             'tenant_n': snaps_creds.project_name,
186             'user_n': snaps_creds.username,
187             'project_domain_n': snaps_creds.project_domain_name,
188             'user_domain_n': snaps_creds.user_domain_name}
189         with open(credentials_yaml, 'w') as yfile:
190             yfile.write(CREDS_TEMPLATE3.format(**creds_data))
191         cmd = ['juju', 'add-credential', 'abot-epc', '-f', credentials_yaml,
192                '--replace']
193         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
194         self.__logger.info("%s\n%s", " ".join(cmd), output)
195
196     def _add_custom_rule(self, sec_grp_name):
197         """ To add custom rule for SCTP Traffic """
198         sec_grp_rules = list()
199         sec_grp_rules.append(
200             SecurityGroupRuleConfig(
201                 sec_grp_name=sec_grp_name, direction=Direction.ingress,
202                 protocol=Protocol.sctp))
203         security_group = OpenStackSecurityGroup(
204             self.snaps_creds,
205             SecurityGroupConfig(
206                 name=sec_grp_name,
207                 rule_settings=sec_grp_rules))
208         security_group.create()
209         self.created_object.append(security_group)
210
211     def prepare(self):
212         """Prepare testcase (Additional pre-configuration steps)."""
213         self.__logger.info("Additional pre-configuration steps")
214         super(JujuEpc, self).prepare()
215         try:
216             os.makedirs(self.res_dir)
217         except OSError as ex:
218             if ex.errno != errno.EEXIST:
219                 self.__logger.exception("Cannot create %s", self.res_dir)
220                 raise vnf.VnfPreparationException
221
222         self.__logger.info("ENV:\n%s", env.string())
223
224         self.public_auth_url = keystone_utils.get_endpoint(
225             self.snaps_creds, 'identity')
226
227         # it enforces a versioned public identity endpoint as juju simply
228         # adds /auth/tokens wich fails vs an unversioned endpoint.
229         if not self.public_auth_url.endswith(('v3', 'v3/', 'v2.0', 'v2.0/')):
230             self.public_auth_url = urljoin(self.public_auth_url, 'v3')
231         self._register_cloud()
232         if self.snaps_creds.identity_api_version == 3:
233             self._register_credentials_v3()
234         else:
235             self._register_credentials_v2()
236
237     def deploy_orchestrator(self):  # pylint: disable=too-many-locals
238         """
239         Create network, subnet, router
240
241         Bootstrap juju
242         """
243         self.__logger.info("Deploying Juju Orchestrator")
244         private_net_name = getattr(
245             config.CONF, 'vnf_{}_private_net_name'.format(self.case_name))
246         private_subnet_name = '{}-{}'.format(
247             getattr(config.CONF,
248                     'vnf_{}_private_subnet_name'.format(self.case_name)),
249             self.uuid)
250         private_subnet_cidr = getattr(
251             config.CONF, 'vnf_{}_private_subnet_cidr'.format(self.case_name))
252         abot_router = '{}-{}'.format(
253             getattr(config.CONF,
254                     'vnf_{}_external_router'.format(self.case_name)),
255             self.uuid)
256         self.__logger.info("Creating full network with nameserver: %s",
257                            env.get('NAMESERVER'))
258         subnet_settings = SubnetConfig(
259             name=private_subnet_name,
260             cidr=private_subnet_cidr,
261             dns_nameservers=[env.get('NAMESERVER')])
262         network_settings = NetworkConfig(
263             name=private_net_name, subnet_settings=[subnet_settings])
264         network_creator = OpenStackNetwork(self.snaps_creds, network_settings)
265         net_id = network_creator.create().id
266         self.created_object.append(network_creator)
267
268         ext_net_name = snaps_utils.get_ext_net_name(self.snaps_creds)
269         self.__logger.info("Creating network Router ....")
270         router_creator = OpenStackRouter(
271             self.snaps_creds, RouterConfig(
272                 name=abot_router,
273                 external_gateway=ext_net_name,
274                 internal_subnets=[subnet_settings.name]))
275         router_creator.create()
276         self.created_object.append(router_creator)
277         self.__logger.info("Creating Flavor ....")
278         flavor_settings = FlavorConfig(
279             name=self.orchestrator['requirements']['flavor']['name'],
280             ram=self.orchestrator['requirements']['flavor']['ram_min'],
281             disk=10, vcpus=1)
282         flavor_creator = OpenStackFlavor(self.snaps_creds, flavor_settings)
283         flavor_creator.create()
284         self.created_object.append(flavor_creator)
285
286         self.__logger.info("Upload some OS images if it doesn't exist")
287         images = get_config("tenant_images", self.config_file)
288         self.__logger.info("Images needed for vEPC: %s", images)
289         for image_name, image_file in images.iteritems():
290             self.__logger.info("image: %s, file: %s", image_name, image_file)
291             if image_file and image_name:
292                 image_creator = OpenStackImage(self.snaps_creds, ImageConfig(
293                     name=image_name, image_user='cloud', img_format='qcow2',
294                     image_file=image_file))
295                 image_id = image_creator.create().id
296                 cmd = ['juju', 'metadata', 'generate-image', '-d', '/root',
297                        '-i', image_id, '-s', image_name,
298                        '-r', self.snaps_creds.region_name,
299                        '-u', self.public_auth_url]
300                 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
301                 self.__logger.info("%s\n%s", " ".join(cmd), output)
302                 self.created_object.append(image_creator)
303         self.__logger.info("Network ID  : %s", net_id)
304
305         self.__logger.info("Starting Juju Bootstrap process...")
306         try:
307             cmd = ['timeout', '-t', JujuEpc.juju_timeout,
308                    'juju', 'bootstrap', 'abot-epc', 'abot-controller',
309                    '--metadata-source', '/root',
310                    '--constraints', 'mem=2G',
311                    '--bootstrap-series', 'xenial',
312                    '--config', 'network={}'.format(net_id),
313                    '--config', 'ssl-hostname-verification=false',
314                    '--config', 'use-floating-ip=true',
315                    '--config', 'use-default-secgroup=true',
316                    '--debug']
317             output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
318             self.__logger.info("%s\n%s", " ".join(cmd), output)
319         except subprocess.CalledProcessError as cpe:
320             self.__logger.error(
321                 "Exception with Juju Bootstrap: %s\n%s",
322                 cpe.cmd, cpe.output)
323             return False
324         except Exception:  # pylint: disable=broad-except
325             self.__logger.exception("Some issue with Juju Bootstrap ...")
326             return False
327
328         return True
329
330     def check_app(self, name='abot-epc-basic', status='active'):
331         """Check application status."""
332         cmd = ['juju', 'status', '--format', 'short', name]
333         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
334         self.__logger.info("%s\n%s", " ".join(cmd), output)
335         ret = re.search(r'(?=workload:({})\))'.format(status), output)
336         if ret:
337             self.__logger.info("%s workload is %s", name, status)
338             return True
339         self.__logger.error("%s workload differs from %s", name, status)
340         return False
341
342     def deploy_vnf(self):
343         """Deploy ABOT-OAI-EPC."""
344         self.__logger.info("Upload VNFD")
345         descriptor = self.vnf['descriptor']
346         self.__logger.info("Get or create flavor for all Abot-EPC")
347         flavor_settings = FlavorConfig(
348             name=self.vnf['requirements']['flavor']['name'],
349             ram=self.vnf['requirements']['flavor']['ram_min'],
350             disk=10,
351             vcpus=1)
352         flavor_creator = OpenStackFlavor(self.snaps_creds, flavor_settings)
353         flavor_creator.create()
354         self.created_object.append(flavor_creator)
355         self.__logger.info("Deploying Abot-epc bundle file ...")
356         cmd = ['juju', 'deploy', '{}'.format(descriptor.get('file_name'))]
357         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
358         self.__logger.info("%s\n%s", " ".join(cmd), output)
359         self.__logger.info("Waiting for instances .....")
360         try:
361             cmd = ['timeout', '-t', JujuEpc.juju_timeout, 'juju-wait']
362             output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
363             self.__logger.info("%s\n%s", " ".join(cmd), output)
364             self.__logger.info("Deployed Abot-epc on Openstack")
365         except subprocess.CalledProcessError as cpe:
366             self.__logger.error(
367                 "Exception with Juju VNF Deployment: %s\n%s",
368                 cpe.cmd, cpe.output)
369             return False
370         except Exception:  # pylint: disable=broad-except
371             self.__logger.exception("Some issue with the VNF Deployment ..")
372             return False
373
374         self.__logger.info("Checking status of ABot and EPC units ...")
375         cmd = ['juju', 'status']
376         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
377         self.__logger.debug("%s\n%s", " ".join(cmd), output)
378         for app in ['abot-epc-basic', 'oai-epc', 'oai-hss']:
379             if not self.check_app(app):
380                 return False
381
382         nova_client = nova_utils.nova_client(self.snaps_creds)
383         instances = get_instances(nova_client)
384         self.__logger.info("List of Instance: %s", instances)
385         for items in instances:
386             metadata = get_instance_metadata(nova_client, items)
387             if 'juju-units-deployed' in metadata:
388                 sec_group = 'juju-{}-{}'.format(
389                     metadata['juju-controller-uuid'],
390                     metadata['juju-model-uuid'])
391                 self.__logger.info("Instance: %s", sec_group)
392                 break
393         self.__logger.info("Adding Security group rule....")
394         # This will add sctp rule to a common Security Group Created
395         # by juju and shared to all deployed units.
396         self._add_custom_rule(sec_group)
397
398         self.__logger.info("Transferring the feature files to Abot_node ...")
399         cmd = ['timeout', '-t', JujuEpc.juju_timeout,
400                'juju', 'scp', '--', '-r', '-v',
401                '{}/featureFiles'.format(self.case_dir), 'abot-epc-basic/0:~/']
402         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
403         self.__logger.info("%s\n%s", " ".join(cmd), output)
404
405         self.__logger.info("Copying the feature files within Abot_node ")
406         cmd = ['timeout', '-t', JujuEpc.juju_timeout,
407                'juju', 'ssh', 'abot-epc-basic/0',
408                'sudo', 'cp', '-vfR', '~/featureFiles/*',
409                '/etc/rebaca-test-suite/featureFiles']
410         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
411         self.__logger.info("%s\n%s", " ".join(cmd), output)
412         return True
413
414     def test_vnf(self):
415         """Run test on ABoT."""
416         start_time = time.time()
417         self.__logger.info("Running VNF Test cases....")
418         cmd = ['juju', 'run-action', 'abot-epc-basic/0', 'run',
419                'tagnames={}'.format(self.details['test_vnf']['tag_name'])]
420         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
421         self.__logger.info("%s\n%s", " ".join(cmd), output)
422
423         cmd = ['timeout', '-t', JujuEpc.juju_timeout, 'juju-wait']
424         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
425         self.__logger.info("%s\n%s", " ".join(cmd), output)
426
427         duration = time.time() - start_time
428         self.__logger.info("Getting results from Abot node....")
429         cmd = ['timeout', '-t', JujuEpc.juju_timeout,
430                'juju', 'scp', '--', '-v',
431                'abot-epc-basic/0:'
432                '/var/lib/abot-epc-basic/artifacts/TestResults.json',
433                '{}/.'.format(self.res_dir)]
434         output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
435         self.__logger.info("%s\n%s", " ".join(cmd), output)
436         self.__logger.info("Parsing the Test results...")
437         res = (process_abot_test_result('{}/TestResults.json'.format(
438             self.res_dir)))
439         short_result = sig_test_format(res)
440         self.__logger.info(short_result)
441         self.details['test_vnf'].update(
442             status='PASS', result=short_result, full_result=res,
443             duration=duration)
444         self.__logger.info(
445             "Test VNF result: Passed: %d, Failed:%d, Skipped: %d",
446             short_result['passed'],
447             short_result['failures'], short_result['skipped'])
448         return True
449
450     def clean(self):
451         """Clean created objects/functions."""
452         try:
453             cmd = ['juju', 'debug-log', '--replay', '--no-tail']
454             output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
455             self.__logger.debug("%s\n%s", " ".join(cmd), output)
456             if not self.orchestrator['requirements']['preserve_setup']:
457                 self.__logger.info("Destroying Orchestrator...")
458                 cmd = ['timeout', '-t', JujuEpc.juju_timeout,
459                        'juju', 'destroy-controller', '-y', 'abot-controller',
460                        '--destroy-all-models']
461                 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
462                 self.__logger.info("%s\n%s", " ".join(cmd), output)
463         except subprocess.CalledProcessError as cpe:
464             self.__logger.error(
465                 "Exception with Juju Cleanup: %s\n%s",
466                 cpe.cmd, cpe.output)
467         except Exception:  # pylint: disable=broad-except
468             self.__logger.exception("General issue during the undeployment ..")
469
470         if not self.orchestrator['requirements']['preserve_setup']:
471             self.__logger.info('Remove the Abot_epc OS object ..')
472             super(JujuEpc, self).clean()
473
474         return True
475
476
477 # ----------------------------------------------------------
478 #
479 #               YAML UTILS
480 #
481 # -----------------------------------------------------------
482 def get_config(parameter, file_path):
483     """
484     Returns the value of a given parameter in file.yaml
485     parameter must be given in string format with dots
486     Example: general.openstack.image_name
487     """
488     with open(file_path) as config_file:
489         file_yaml = yaml.safe_load(config_file)
490     config_file.close()
491     value = file_yaml
492     for element in parameter.split("."):
493         value = value.get(element)
494         if value is None:
495             raise ValueError("The parameter %s is not defined in"
496                              " reporting.yaml" % parameter)
497     return value
498
499
500 def sig_test_format(sig_test):
501     """
502     Process the signaling result to have a short result
503     """
504     nb_passed = 0
505     nb_failures = 0
506     nb_skipped = 0
507     for data_test in sig_test:
508         if data_test['result'] == "passed":
509             nb_passed += 1
510         elif data_test['result'] == "failed":
511             nb_failures += 1
512         elif data_test['result'] == "skipped":
513             nb_skipped += 1
514     total_sig_test_result = {}
515     total_sig_test_result['passed'] = nb_passed
516     total_sig_test_result['failures'] = nb_failures
517     total_sig_test_result['skipped'] = nb_skipped
518     return total_sig_test_result
519
520
521 def process_abot_test_result(file_path):
522     """ Process ABoT Result """
523     with open(file_path) as test_result:
524         data = json.load(test_result)
525         res = []
526         for tests in data:
527             tests = update_data(tests)
528             try:
529                 flatten_steps = tests['elements'][0].pop('flatten_steps')
530                 for steps in flatten_steps:
531                     steps['result'] = steps['step_status']
532                     res.append(steps)
533             except:
534                 logging.error("Could not post data to ElasticSearch host")
535                 raise
536         return res
537
538
539 def update_data(obj):
540     """ Update Result data"""
541     try:
542         obj['feature_file'] = os.path.splitext(os.path.basename(obj['uri']))[0]
543
544         for element in obj['elements']:
545             element['final_result'] = "passed"
546             element['flatten_steps'] = []
547
548             for step in element['steps']:
549                 temp_dict = {}
550                 step['result'][step['result']['status']] = 1
551                 if step['result']['status'].lower() in ['fail', 'failed']:
552                     element['final_result'] = "failed"
553
554                 temp_dict['feature_file'] = obj['feature_file']
555                 temp_dict['step_name'] = step['name']
556                 temp_dict['step_status'] = step['result']['status']
557                 temp_dict['step_duration'] = step['result'].get('duration', 0)
558                 temp_dict['step_' + step['result']['status']] = 1
559                 element['flatten_steps'].append(deepcopy(temp_dict))
560
561             # Need to put the tag in OBJ and not ELEMENT
562             if 'tags' in obj:
563                 element['tags'] = deepcopy(obj['tags'])
564                 for tag in obj['tags']:
565                     element[tag['name']] = 1
566             else:
567                 for tag in element['tags']:
568                     element[tag['name']] = 1
569
570     except Exception:  # pylint: disable=broad-except
571         logging.error("Error in updating data, %s", sys.exc_info()[0])
572         raise
573
574     return obj
575
576
577 def get_instances(nova_client):
578     """ To get all vm info of a project """
579     try:
580         instances = nova_client.servers.list()
581         return instances
582     except Exception as exc:  # pylint: disable=broad-except
583         logging.error("Error [get_instances(nova_client)]: %s", exc)
584         return None
585
586
587 def get_instance_metadata(nova_client, instance):
588     """ Get instance Metadata - Instance ID """
589     try:
590         instance = nova_client.servers.get(instance.id)
591         return instance.metadata
592     except Exception as exc:  # pylint: disable=broad-except
593         logging.error("Error [get_instance_status(nova_client)]: %s", exc)
594         return None