def stop(softlayer_client, retry_interval_secs, **_): vm_details = _get_vm_details() instance_id = vm_details[constants.INSTANCE_ID] vm_details_to_print = _get_vm_details_for_print(vm_details) # retry if there is an active transaction virtual_guest_service = softlayer_client[constants.VIRTUAL_GUEST] transaction_name = _get_transaction_full_name( get_active_transaction(virtual_guest_service, instance_id)) if transaction_name: return ctx.operation.retry( message='host [{0}] has active transaction [{1}], ' 'waiting for transactions to end.. ' 'operation will be retried'.format(vm_details_to_print, transaction_name), retry_after=retry_interval_secs) # stopping the server if the state is RUNNING state = virtual_guest_service.getPowerState( id=instance_id)[constants.POWER_STATE_KEY_NAME] if state == constants.RUNNING_STATE: ctx.logger.info('stopping server [{0}]'.format(vm_details_to_print)) ok = virtual_guest_service.powerOff(id=instance_id) state = virtual_guest_service.getPowerState( id=instance_id)[constants.POWER_STATE_KEY_NAME] if ok and state == constants.HALTED_STATE: ctx.logger.info('server [{0}] was stopped successfully'.format( vm_details_to_print)) ctx.instance.runtime_properties[ constants.STATUS] = constants.STOPPED else: raise NonRecoverableError( 'failed to stop server [{0}], power state: {1}'.format( vm_details_to_print, state)) else: # state is not RUNNING ctx.logger.info('server [{0}] is not running, ' 'power state is {1}, ' 'stop will not be performed.'.format( vm_details_to_print, state))
def start_monitoring_hpc(config, credentials, external_monitor_entrypoint, external_monitor_port, external_monitor_orchestrator_port, simulate, **kwargs): # pylint: disable=W0613 """ Starts monitoring using the Monitor orchestrator """ external_monitor_entrypoint = None # FIXME: external monitor disabled if external_monitor_entrypoint: ctx.logger.info('Starting infrastructure monitor..') if not simulate: if 'credentials' in ctx.instance.runtime_properties: credentials = ctx.instance.runtime_properties['credentials'] infrastructure_interface = config['infrastructure_interface'] country_tz = config['country_tz'] url = 'http://' + external_monitor_entrypoint + \ external_monitor_orchestrator_port + '/exporters/add' # FIXME: credentials doesn't have to have a password anymore payload = ("{\n\t\"host\": \"" + credentials['host'] + "\",\n\t\"type\": \"" + infrastructure_interface + "\",\n\t\"persistent\": false,\n\t\"args\": {\n\t\t\"" "user\": \"" + credentials['user'] + "\",\n\t\t\"" "pass\": \"" + credentials['password'] + "\",\n\t\t\"" "tz\": \"" + country_tz + "\",\n\t\t\"" "log\": \"debug\"\n\t}\n}") headers = { 'content-type': "application/json", 'cache-control': "no-cache", } response = requests.request("POST", url, data=payload, headers=headers) if response.status_code != 201: raise NonRecoverableError("failed to start node monitor: " + str(response.status_code)) else: ctx.logger.warning('monitor simulated')
def start(self, args=None, start_retry_interval=30, private_key_path=None, **_): instance_id = self.resource_id self._assign_runtime_properties_to_instance( runtime_properties=constants.INSTANCE_INTERNAL_ATTRIBUTES) if self._get_instance_state() == constants.INSTANCE_STATE_STARTED: if ctx.node.properties['use_password']: password_success = self._retrieve_windows_pass( instance_id=instance_id, private_key_path=private_key_path) if not password_success: return False return True ctx.logger.debug( 'Attempting to start instance: {0}.)'.format(instance_id)) try: self.execute(self.client.start_instances, dict(instance_ids=instance_id), raise_on_falsy=True) except (exception.EC2ResponseError, exception.BotoServerError) as e: raise NonRecoverableError('{0}'.format(str(e))) ctx.logger.debug( 'Attempted to start instance {0}.'.format(instance_id)) if self._get_instance_state() == constants.INSTANCE_STATE_STARTED: if ctx.node.properties['use_password']: password_success = self._retrieve_windows_pass( instance_id=instance_id, private_key_path=private_key_path) if not password_success: return False else: return False return True
def poststart(resource, ctx): """ Read a system resource and store its properties in the node instance runtime properties. :param resource: A system resource. :param ctx: The Cloudify context. :return: """ if resource.is_subcloud: update_subcloud_resource(resource, ctx.instance, ctx.deployment.id) add_new_label('csys-env-type', LABELS['types']['subcloud'], ctx.deployment.id) elif resource.is_system_controller: if 'subcloud_names' not in ctx.instance.runtime_properties: ctx.instance.runtime_properties['subcloud_names'] = [] for subcloud_name in resource.subcloud_resource_names: ctx.instance.runtime_properties['subcloud_names'].append( subcloud_name) # update_prop_resources( # ctx.instance, resource.subcloud_resources, 'subclouds') add_new_label('csys-env-type', LABELS['types']['systemcontroller'], ctx.deployment.id) elif not resource.is_standalone_system: raise NonRecoverableError( 'Unsupported system type: ' 'the system is neither a standalone system, system controller, ' 'nor a subcloud.') else: add_new_label('csys-env-type', LABELS['types']['default'], ctx.deployment.id) update_prop_resource(ctx.instance, resource) update_prop_resources(ctx.instance, resource.host_resources, 'hosts') update_prop_resources(ctx.instance, resource.kube_cluster_resources, 'kube_clusters') update_kubernetes_props(ctx.instance, resource.kube_cluster_resources) update_openstack_props(ctx.instance, resource.openstack_cluster_resource, resource.client_config) assign_required_labels(ctx.instance, ctx.deployment.id) assign_site(ctx.instance, ctx.deployment.id, resource.location)
def validate_resource_quota(resource, openstack_type): """ Do a validation for openstack resource to make sure it is allowed to create resource based on available resources created and maximum quota :param resource: openstack resource instance :param openstack_type: openstack resource type """ ctx.logger.info('validating resource {0} (node {1})' ''.format(openstack_type, ctx.node.id)) openstack_type_plural = resource.resource_plural(openstack_type) resource_list = list(resource.list()) # This is the available quota for provisioning the resource resource_amount = len(resource_list) # Log message to give an indication to the caller that there will be a # call trigger to fetch the quota for current resource ctx.logger.info('Fetching quota for resource {0} (node {1})' ''.format(openstack_type, ctx.node.id)) # This represent the quota for the provided resource openstack type resource_quota = resource.get_quota_sets(openstack_type_plural) if resource_amount < resource_quota \ or resource_quota == INFINITE_RESOURCE_QUOTA: ctx.logger.debug( QUOTA_VALID_MSG.format(openstack_type, ctx.node.id, openstack_type_plural, resource_amount, resource_quota)) else: err_message = \ QUOTA_INVALID_MSG.format( openstack_type, ctx.node.id, openstack_type_plural, resource_amount, resource_quota ) ctx.logger.error('VALIDATION ERROR: {0}'.format(err_message)) raise NonRecoverableError(err_message)
def snapshot_apply(nova_client, glance_client, **kwargs): """ Create server backup. """ server = get_server_by_context(nova_client) snapshot_name = _get_snapshot_name(ctx, kwargs) snapshot_incremental = kwargs["snapshot_incremental"] if snapshot_incremental: ctx.logger.info("Apply snapshot {} for {}".format( snapshot_name, server.human_id)) else: ctx.logger.info("Apply backup {} for {}".format( snapshot_name, server.human_id)) image_id, _ = _get_image(glance_client, snapshot_name, snapshot_incremental) if not image_id: raise NonRecoverableError( "No snapshots found with name: {}.".format(snapshot_name)) _check_finished_upload(nova_client, server, ['image_uploading', 'rebuild_spawning']) restorestate = ctx.instance.runtime_properties.get("restorestate") if restorestate != snapshot_name: # we stop before restore _server_stop(nova_client, server) ctx.logger.info("Rebuild {} with {}".format(server.human_id, snapshot_name)) server.rebuild(image_id) ctx.instance.runtime_properties["restorestate"] = snapshot_name # we have applied backup so we can start instance server = nova_client.servers.get(server.id) _check_finished_upload(nova_client, server, ['rebuild_spawning']) _server_start(nova_client, server) ctx.instance.runtime_properties["restorestate"] = "done"
def cleanup_job(job_options, skip, **kwargs): # pylint: disable=W0613 """Clean the aux files of the job in the HPC""" if skip: return simulate = ctx.instance.runtime_properties['simulate'] name = kwargs['name'] if not simulate: is_singularity = 'hpc.nodes.singularity_job' in ctx.node.\ type_hierarchy credentials = ctx.instance.runtime_properties['credentials'] workdir = ctx.instance.runtime_properties['workdir'] wm_type = ctx.instance.runtime_properties['workload_manager'] client = SshClient(credentials['host'], credentials['user'], credentials['password'], use_login_shell=credentials['login_shell']) # TODO(emepetres): manage errors wm = WorkloadManager.factory(wm_type) if not wm: raise NonRecoverableError("Workload Manager '" + wm_type + "' not supported.") is_clean = wm.clean_job_aux_files(client, name, job_options, is_singularity, ctx.logger, workdir=workdir) client.close_connection() else: ctx.logger.warning('Instance ' + ctx.instance.id + ' simulated') is_clean = True if is_clean: ctx.logger.info('Job ' + name + ' (' + ctx.instance.id + ') cleaned.') else: ctx.logger.error('Job ' + name + ' (' + ctx.instance.id + ') not cleaned.')
def wait_status(ctx, resource, expected_status=constants.SUCCEEDED, timeout=600): """ A helper to send request to Azure. The operation status is check to monitor the request. Failures are managed too. :param ctx: The Cloudify context to log information. :param resource: The resource to waiting for. :param expected_status: The expected status for the operation. :param timeout: Maximum time to wait in seconds. """ module = importlib.import_module('azurecloudify.{0}'.format(resource), package=None) ctx.logger.debug('Waiting for status {0} for {1}...'.format( expected_status, resource)) attempt_index = 1 waiting_time = 0 status = getattr(module, 'get_provisioning_state')(ctx=ctx) ctx.logger.info('{0} status is {1}...'.format(resource, status)) while (status != expected_status) and (status != constants.FAILED) and ( waiting_time <= timeout): waiting_time += constants.WAITING_TIME sleep(constants.WAITING_TIME) status = getattr(module, 'get_provisioning_state')(ctx=ctx) attempt_index += 1 ctx.logger.info('{0} status is {1} - attempt #{2}...'.format( resource, status, attempt_index)) if status != expected_status: if waiting_time >= timeout: message = 'Timeout occurs while waiting status {0} for {1}...'.format( expected_status, resource) else: message = '*** Failed waiting {0} for {1}: {2} ***'.format( expected_status, resource, status) raise NonRecoverableError(message) else: ctx.logger.info("** {0}'s status ({1}) is as expected. **".format( resource, status))
def _update_subnet_config(subnet_config): """ This method will try to update subnet config with network configurations using the relationships connected with subnet node :param dict subnet_config: The subnet configuration required in order to create the subnet instance using Openstack API """ # Check to see if the network id is provided on the subnet config # properties network_id = subnet_config.get('network_id') # Get the network id from relationship if it is existed rel_network_id = _get_subnet_network_id_from_relationship() if network_id and rel_network_id: raise NonRecoverableError('Subnet can\'t both have the ' '"network_id" property and be ' 'connected to a network via a ' 'relationship at the same time') subnet_config['network_id'] = network_id or rel_network_id
def resolve_node_ctx_from_relationship(_ctx): """ This method is to decide where to get node from relationship context since this is not exposed correctly from cloudify :param _ctx: current cloudify context object :return: RelationshipSubjectContext instance """ # Get the node_id for the current node in order to decide if that node # is source | target node_id = _ctx._context.get('node_id') source_node_id = _ctx.source._context.get('node_id') target_node_id = _ctx.target._context.get('node_id') if node_id == source_node_id: return _ctx.source elif node_id == target_node_id: return _ctx.target else: raise NonRecoverableError( 'Unable to decide if current node is source or target')
def _get_volumes(list_of_volume_ids): """Returns a list of EBS Volumes for a given list of volume IDs. :param list_of_volume_ids: A list of EBS volume IDs. :returns A list of EBS objects. :raises NonRecoverableError: If Boto errors. """ ec2_client = connection.EC2ConnectionClient().client() try: volumes = ec2_client.get_all_volumes(volume_ids=list_of_volume_ids) except boto.exception.EC2ResponseError as e: if 'InvalidVolume.NotFound' in e: all_volumes = ec2_client.get_all_volumes() utils.log_available_resources(all_volumes) return None except boto.exception.BotoServerError as e: raise NonRecoverableError('{0}'.format(str(e))) return volumes
def start_ctx_proxy(ctx, process): ctx_proxy_type = process.get('ctx_proxy_type') if not ctx_proxy_type or ctx_proxy_type == 'auto': if HAS_ZMQ: if IS_WINDOWS: return TCPCtxProxy(ctx) else: return UnixCtxProxy(ctx) else: return HTTPCtxProxy(ctx) elif ctx_proxy_type == 'unix': return UnixCtxProxy(ctx) elif ctx_proxy_type == 'tcp': return TCPCtxProxy(ctx) elif ctx_proxy_type == 'http': return HTTPCtxProxy(ctx) elif ctx_proxy_type == 'none': return StubCtxProxy() else: raise NonRecoverableError( 'Unsupported proxy type: {0}'.format(ctx_proxy_type))
def get_resource_by_name_or_id(resource_id, openstack_type, sugared_client, raise_if_not_found=True, name_field_name='name'): # search for resource by name (or name-equivalent field) search_param = {name_field_name: resource_id} resource = sugared_client.cosmo_get_if_exists(openstack_type, **search_param) if not resource: # fallback - search for resource by id resource = sugared_client.cosmo_get_if_exists(openstack_type, id=resource_id) if not resource and raise_if_not_found: raise NonRecoverableError( "Couldn't find a resource of type {0} with the name or id {1}". format(openstack_type, resource_id)) return resource
def _put_api_in_kwargs(api_name, kwargs): if api_name in kwargs and not isinstance(kwargs[api_name], RESTApi): raise NonRecoverableError('Incorrect API class exists.') ctx = _get_ctx(kwargs) auth = None if ctx.type == context.NODE_INSTANCE: auth = ctx.node.properties.get(PROP_CLIENT_CONFIG) elif ctx.type == context.RELATIONSHIP_INSTANCE: auth = ctx.source.node.properties.get(PROP_CLIENT_CONFIG) if not auth: auth = ctx.target.node.properties.get(PROP_CLIENT_CONFIG) if PROP_CLIENT_CONFIG in kwargs: try: auth = auth.copy() auth.update(kwargs[PROP_CLIENT_CONFIG]) except AttributeError: auth = kwargs[PROP_CLIENT_CONFIG] kwargs[api_name] = RESTApi(auth, logger=ctx.logger)
def _http_client_wrapper(self, option, request_action, request_args={}): """ wrapper for http client requests with CloudifyClientError custom handling. :param option: can be blueprints, executions and etc. :param request_action: action to be done, like list, get and etc. :param request_args: args for the actual call. :return: The http response. """ generic_client = getattr(self.client, option) option_client = getattr(generic_client, request_action) try: return option_client(**request_args) except CloudifyClientError as ex: raise NonRecoverableError( 'Client action "{0}" failed: {1}.'.format(request_action, ex))
def edit_docker_config(flannel): if not flannel: return ctx.operation.retry( 'Flannel is empty {0}'.format(flannel) ) with open('/tmp/docker', 'w') as fd: with open('/etc/default/docker', 'r') as fdin: for line in fdin: fd.write(line) with open('/tmp/docker', 'a') as fd: fd.write('{0}\n' '{1}\n'.format(flannel, CMD_APP) ) try: subprocess.call('sudo mv /tmp/docker /etc/default/docker', shell=True) except: raise NonRecoverableError('Unable to move Docker config into place.')
def get_broker_ssl_and_port(ssl_enabled, cert_path): # Input vars may be None if not set. Explicitly defining defaults. ssl_enabled = ssl_enabled or False cert_path = cert_path or '' if ssl_enabled: if not cert_path: raise NonRecoverableError( "Broker SSL enabled but no SSL cert was provided. " "If rabbitmq_ssl_enabled is True in the inputs, " "rabbitmq_cert_public (and private) must be populated.") port = constants.BROKER_PORT_SSL ssl_options = { 'ca_certs': cert_path, 'cert_reqs': ssl.CERT_REQUIRED, } else: port = constants.BROKER_PORT_NO_SSL ssl_options = {} return port, ssl_options
def generate_deployment_file(): data = dict() # Get the deployments info for Nodes && Load balancers deployments = get_kubernetes_deployment_info( ctx.instance.relationships, 'cloudify.relationships.depends_on', 'cloudify.nodes.DeploymentProxy') if deployments: deployment_file = os.path.expanduser('~') + "/deployment.json" data['deployments'] = deployments if not os.path.isfile(deployment_file): ctx.logger.info("Create deployment {}" " file".format(deployment_file)) with open(deployment_file, 'w') as json_file: json.dump(data, json_file) return raise NonRecoverableError('Unable to generate deployment file, ' 'deployment ids are empty !!!.' 'Please check your kubernetes.yaml')
def read(self, name=None, cid=None): ''' Reads in a FortiGate config entry :param string name: Name of the configuration element (eg. "firewall policy") :param string cid: Name of the specific configuration ID to delete ''' name = name or self.name cid = cid or self.cid self.ctx.logger.info('Running show config on "%s (%s)"', name, cid) # Sanity checks if not name: raise NonRecoverableError('Missing config name parameter') # Run the command command = 'show {0}'.format(name) if cid: command = '{0} {1}'.format(command, cid) return self.parse_output( self.execute('show {0} {1}'.format(name, cid)))
def test_run_auth_relationship(self): _ctx = self._gen_relation_ctx() ssh_mock = Mock() ssh_mock.connect = Mock(side_effect=OSError("e")) try: type(_ctx.target.instance).host_ip = PropertyMock( side_effect=NonRecoverableError('host_ip is undefined')) with patch("paramiko.SSHClient", Mock(return_value=ssh_mock)): with self.assertRaises(OperationRetry): tasks.run( ctx=_ctx, calls=[{'action': 'ls'}], logger_file="/tmp/terminal.log", terminal_auth=json.loads(json.dumps( {'ip': 'ip', 'user': '******', 'password': '******'}))) ssh_mock.connect.assert_called_with( 'ip', allow_agent=False, look_for_keys=False, password='******', port=22, timeout=5, username='******') finally: type(_ctx.target.instance).host_ip = None
def _verify_core_down(deployment_config_dir_path): timeout = 60 ok_path = path.join(deployment_config_dir_path, 'ok') end = time.time() + timeout while time.time() < end: # after the core is stopped this file is removed if os.path.isfile(ok_path): time.sleep(0.5) else: return try: riemann_log_output = subprocess.check_output( 'tail -n 100 {}'.format(RIEMANN_LOG_PATH), shell=True) except Exception as e: riemann_log_output = 'Failed extracting log: {0}'.format(e) raise NonRecoverableError('Riemann core has not stopped in {} seconds.\n' 'tail -n 100 {}:\n {}'.format( timeout, RIEMANN_LOG_PATH, riemann_log_output))
def _execute(params, template_file, retry_count, retry_sleep, ctx, **kwargs): if not template_file: ctx.logger.info('Processing finished. No template file provided.') return template = (kwargs['resource_callback'](template_file)).decode("utf-8") try: kwargs['params'] = params kwargs['template'] = template rerun(ctx=ctx, func=_execute_in_retry, args=[], kwargs=kwargs, retry_count=retry_count, retry_sleep=retry_sleep) except (NonRecoverableError, RecoverableError) as e: ctx.logger.debug("Raised: {e}".format(e=e)) raise e except Exception as e: ctx.logger.info('Exception traceback : {}'.format( traceback.format_exc())) raise NonRecoverableError(e)
def _get_all_addresses(address=None): """Returns a list of elastip objects for a given address elastip. :param address: The ID of a elastip. :returns A list of elasticip objects. :raises NonRecoverableError: If Boto errors. """ ec2_client = connection.EC2ConnectionClient().client() try: addresses = ec2_client.get_all_addresses(address) except boto.exception.EC2ResponseError as e: if 'InvalidAddress.NotFound' in e: addresses = ec2_client.get_all_addresses() utils.log_available_resources(addresses, ctx.logger) return None except boto.exception.BotoServerError as e: raise NonRecoverableError('{0}'.format(str(e))) return addresses
def discover_resources(node_id=None, resource_types=None, zones=None, ctx=None, **_): discovered_resources = {} ctx = ctx or wtx node_id = node_id or get_gcp_account_node_id(ctx.nodes) node = ctx.get_node(node_id) for node_instance in node.instances: if not isinstance(zones, list) and not zones: zones = get_zones(node, ctx.deployment.id) resources = get_resources(node, zones, resource_types, ctx.logger) discovered_resources.update(resources) node_instance._node_instance.runtime_properties['resources'] = \ resources return discovered_resources raise NonRecoverableError( 'No node instances of the provided node ID {n} exist. ' 'Please install the account blueprint.'.format(n=node_id))
def create(self): """ Create GCP VM instance with given parameters. Zone operation. :return: REST response with operation responsible for the instance creation process and its status :raise: GCPError if there is any problem with startup script file: e.g. the file is not under the given path or it has wrong permissions """ disk = ctx.instance.runtime_properties.get(constants.DISK) if disk: self.disks = [disk] if not self.disks and not self.image: raise NonRecoverableError("A disk image ID must be provided") return self.discovery.instances().insert( project=self.project, zone=basename(self.zone), body=self.to_dict()).execute()
def _restore_snapshot(snapshot_id, restore_params): output = execute_and_log(['cfy', 'snapshots', 'restore', snapshot_id] + restore_params) execution_id = output.split("The execution's id is")[1].strip().split()[0] ctx.logger.info('Waiting for the snapshot to be restored...') snapshot_restored = False for retry in range(1, 101): ctx.logger.info( 'Waiting for the snapshot to be restored [retry {0}/100]'.format( retry)) snapshot_restored = _is_snapshot_restored(execution_id) if snapshot_restored: ctx.logger.info( 'Snapshot {0} created successfully'.format(snapshot_id)) break sleep(10) if not snapshot_restored: raise NonRecoverableError( 'Could not restore snapshot {0}'.format(snapshot_id))
def parse_utc_datetime_absolute(time_expression, timezone=None): """ :param time_expression: a string representing an absolute date and time. The following formats are possible: YYYY-MM-DD HH:MM, HH:MM :param timezone: a string representing a timezone. Any timezone recognized by UNIX can be used here, e.g. 'EST' or Asia/Jerusalem. :return: A naive datetime object, in UTC time. """ if not time_expression: return None date_time = parse_schedule_datetime_string(time_expression) if timezone: if timezone not in pytz.all_timezones: raise NonRecoverableError( "{} is not a recognized timezone".format(timezone)) return pytz.timezone(timezone).localize(date_time).astimezone( pytz.utc).replace(tzinfo=None) else: ts = time.time() return date_time - (datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts))
def apply(executable_path, resource_config, **_): """ Execute `terraform apply`. """ tf = Terraform( executable_path, get_terraform_source(resource_config), variables=resource_config.get('variables'), environment_variables=resource_config.get('environment_variables')) tf_apply = tf.apply() tf_state = tf.state_pull() resources = {} for module in tf_state['modules']: resources.update(module.get('resources')) update_runtime_properties('resources', resources) if not tf_apply: raise NonRecoverableError(ERROR_MESSAGE)
def get_external_resource_id_or_raise(operation, ctx_instance): """Checks if the EXTERNAL_RESOURCE_ID runtime_property is set and returns it. :param operation: A string representing what is happening. :param ctx_instance: The CTX Node-Instance Context. :returns The EXTERNAL_RESOURCE_ID runtime_property for a CTX Instance. :raises NonRecoverableError: If EXTERNAL_RESOURCE_ID has not been set. """ ctx.logger.debug( 'Checking if {0} in instance runtime_properties, for {0} ' 'operation.' .format(constants.EXTERNAL_RESOURCE_ID, operation)) if constants.EXTERNAL_RESOURCE_ID not in ctx_instance.runtime_properties: raise NonRecoverableError( 'Cannot {0} because {1} is not assigned.' .format(operation, constants.EXTERNAL_RESOURCE_ID)) return ctx_instance.runtime_properties[constants.EXTERNAL_RESOURCE_ID]
def update(openstack_resource, args): """ Update openstack host aggregate by passing args dict that contains the info that need to be updated :param openstack_resource: Instance of openstack host aggregate resource :param dict args: dict of information need to be updated """ args = reset_dict_empty_keys(args) if not args: raise NonRecoverableError( 'Unable to update aggregate {0}, ' 'args cannot be empty'.format(openstack_resource.resource_id)) # Check if metadata is exist so that we can update it if needed if 'metadata' in args: metadata = args.pop('metadata') openstack_resource.set_metadata(metadata) # Update only if args has value like (name | availability_zone) if args: openstack_resource.update(args)