# Copyright (c) 2017, 2018, 2019 Oracle and/or its affiliates. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type import logging import logging.config import os import tempfile from datetime import datetime from operator import eq import time try: import yaml import oci from oci.constants import HEADER_NEXT_PAGE from oci.exceptions import ( InvalidConfig, InvalidPrivateKey, MissingPrivateKeyPassphrase, ConfigFileNotFound, ServiceError, MaximumWaitTimeExceeded, ) from oci.identity.identity_client import IdentityClient from oci.object_storage.models import CreateBucketDetails from oci.object_storage.models import UpdateBucketDetails from oci.retry import RetryStrategyBuilder from oci.util import to_dict, Sentinel HAS_OCI_PY_SDK = True except ImportError: HAS_OCI_PY_SDK = False from ansible.module_utils._text import to_bytes from ansible.module_utils.six import iteritems __version__ = "1.6.0-dev" MAX_WAIT_TIMEOUT_IN_SECONDS = 1200 # If a resource is in one of these states it would be considered inactive DEAD_STATES = [ "TERMINATING", "TERMINATED", "FAULTY", "FAILED", "DELETING", "DELETED", "UNKNOWN_ENUM_VALUE", "DETACHING", "DETACHED", ] # If a resource is in one of these states it would be considered available DEFAULT_READY_STATES = [ "AVAILABLE", "ACTIVE", "RUNNING", "PROVISIONED", "ATTACHED", "ASSIGNED", "SUCCEEDED", "PENDING_PROVIDER", ] # If a resource is in one of these states, it would be considered deleted DEFAULT_TERMINATED_STATES = ["TERMINATED", "DETACHED", "DELETED"] def get_common_arg_spec(supports_create=False, supports_wait=False): """ Return the common set of module arguments for all OCI cloud modules. :param supports_create: Variable to decide whether to add options related to idempotency of create operation. :param supports_wait: Variable to decide whether to add options related to waiting for completion. :return: A dict with applicable module options. """ # Note: This method is used by most OCI ansible resource modules during initialization. When making changes to this # method, ensure that no `oci` python sdk dependencies are introduced in this method. This ensures that the modules # can check for absence of OCI Python SDK and fail with an appropriate message. Introducing an OCI dependency in # this method would break that error handling logic. common_args = dict( config_file_location=dict(type="str"), config_profile_name=dict(type="str", default="DEFAULT"), api_user=dict(type="str"), api_user_fingerprint=dict(type="str", no_log=True), api_user_key_file=dict(type="str"), api_user_key_pass_phrase=dict(type="str", no_log=True), auth_type=dict( type="str", required=False, choices=["api_key", "instance_principal"], default="api_key", ), tenancy=dict(type="str"), region=dict(type="str"), ) if supports_create: common_args.update( key_by=dict(type="list", elements="str"), force_create=dict(type="bool", default=False), ) if supports_wait: common_args.update( wait=dict(type="bool", default=True), wait_timeout=dict( type="int", default=MAX_WAIT_TIMEOUT_IN_SECONDS ), wait_until=dict(type="str"), ) return common_args def get_facts_module_arg_spec(filter_by_name=False): # Note: This method is used by most OCI ansible fact modules during initialization. When making changes to this # method, ensure that no `oci` python sdk dependencies are introduced in this method. This ensures that the modules # can check for absence of OCI Python SDK and fail with an appropriate message. Introducing an OCI dependency in # this method would break that error handling logic. facts_module_arg_spec = get_common_arg_spec() if filter_by_name: facts_module_arg_spec.update(name=dict(type="str")) else: facts_module_arg_spec.update(display_name=dict(type="str")) return facts_module_arg_spec def get_oci_config(module, service_client_class=None): """Return the OCI configuration to use for all OCI API calls. The effective OCI configuration is derived by merging any overrides specified for configuration attributes through Ansible module options or environment variables. The order of precedence for deriving the effective configuration dict is: 1. If a config file is provided, use that to setup the initial config dict. 2. If a config profile is specified, use that config profile to setup the config dict. 3. For each authentication attribute, check if an override is provided either through a. Ansible Module option b. Environment variable and override the value in the config dict in that order.""" config = {} config_file = module.params.get("config_file_location") _debug("Config file through module options - {0} ".format(config_file)) if not config_file: if "OCI_CONFIG_FILE" in os.environ: config_file = os.environ["OCI_CONFIG_FILE"] _debug( "Config file through OCI_CONFIG_FILE environment variable - {0}".format( config_file ) ) else: config_file = "~/.oci/config" _debug("Config file (fallback) - {0} ".format(config_file)) config_profile = module.params.get("config_profile_name") if not config_profile: if "OCI_CONFIG_PROFILE" in os.environ: config_profile = os.environ["OCI_CONFIG_PROFILE"] else: config_profile = "DEFAULT" try: config = oci.config.from_file( file_location=config_file, profile_name=config_profile ) except ( ConfigFileNotFound, InvalidConfig, InvalidPrivateKey, MissingPrivateKeyPassphrase, ) as ex: if not _is_instance_principal_auth(module): # When auth_type is not instance_principal, config file is required module.fail_json(msg=str(ex)) else: _debug( "Ignore {0} as the auth_type is set to instance_principal".format( str(ex) ) ) # if instance_principal auth is used, an empty 'config' map is used below. config["additional_user_agent"] = "Oracle-Ansible/{0}".format(__version__) # Merge any overrides through other IAM options _merge_auth_option( config, module, module_option_name="api_user", env_var_name="OCI_USER_ID", config_attr_name="user", ) _merge_auth_option( config, module, module_option_name="api_user_fingerprint", env_var_name="OCI_USER_FINGERPRINT", config_attr_name="fingerprint", ) _merge_auth_option( config, module, module_option_name="api_user_key_file", env_var_name="OCI_USER_KEY_FILE", config_attr_name="key_file", ) _merge_auth_option( config, module, module_option_name="api_user_key_pass_phrase", env_var_name="OCI_USER_KEY_PASS_PHRASE", config_attr_name="pass_phrase", ) _merge_auth_option( config, module, module_option_name="tenancy", env_var_name="OCI_TENANCY", config_attr_name="tenancy", ) _merge_auth_option( config, module, module_option_name="region", env_var_name="OCI_REGION", config_attr_name="region", ) # Redirect calls to home region for IAM service. do_not_redirect = module.params.get( "do_not_redirect_to_home_region", False ) or os.environ.get("OCI_IDENTITY_DO_NOT_REDIRECT_TO_HOME_REGION") if service_client_class == IdentityClient and not do_not_redirect: _debug("Region passed for module invocation - {0} ".format(config["region"])) identity_client = IdentityClient(config) region_subscriptions = identity_client.list_region_subscriptions( config["tenancy"] ).data # Replace the region in the config with the home region. [config["region"]] = [ rs.region_name for rs in region_subscriptions if rs.is_home_region is True ] _debug( "Setting region in the config to home region - {0} ".format( config["region"] ) ) return config def create_service_client(module, service_client_class): """ Creates a service client using the common module options provided by the user. :param module: An AnsibleModule that represents user provided options for a Task :param service_client_class: A class that represents a client to an OCI Service :return: A fully configured client """ config = get_oci_config(module, service_client_class) kwargs = {} if _is_instance_principal_auth(module): try: signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() except Exception as ex: message = ( "Failed retrieving certificates from localhost. Instance principal based authentication is only" "possible from within OCI compute instances. Exception: {0}".format( str(ex) ) ) module.fail_json(msg=message) kwargs["signer"] = signer # XXX: Validate configuration -- this may be redundant, as all Client constructors perform a validation try: oci.config.validate_config(config, **kwargs) except oci.exceptions.InvalidConfig as ic: module.fail_json( msg="Invalid OCI configuration. Exception: {0}".format(str(ic)) ) # Create service client class with the signer client = service_client_class(config, **kwargs) return client def _is_instance_principal_auth(module): # check if auth type is overridden via module params instance_principal_auth = ( "auth_type" in module.params and module.params["auth_type"] == "instance_principal" ) if not instance_principal_auth: instance_principal_auth = ( "OCI_ANSIBLE_AUTH_TYPE" in os.environ and os.environ["OCI_ANSIBLE_AUTH_TYPE"] == "instance_principal" ) return instance_principal_auth def _merge_auth_option( config, module, module_option_name, env_var_name, config_attr_name ): """Merge the values for an authentication attribute from ansible module options and environment variables with the values specified in a configuration file""" _debug("Merging {0}".format(module_option_name)) auth_attribute = module.params.get(module_option_name) _debug( "\t Ansible module option {0} = {1}".format(module_option_name, auth_attribute) ) if not auth_attribute: if env_var_name in os.environ: auth_attribute = os.environ[env_var_name] _debug( "\t Environment variable {0} = {1}".format(env_var_name, auth_attribute) ) # An authentication attribute has been provided through an env-variable or an ansible # option and must override the corresponding attribute's value specified in the # config file [profile]. if auth_attribute: _debug( "Updating config attribute {0} -> {1} ".format( config_attr_name, auth_attribute ) ) config.update({config_attr_name: auth_attribute}) def bucket_details_factory(bucket_details_type, module): bucket_details = None if bucket_details_type == "create": bucket_details = CreateBucketDetails() elif bucket_details_type == "update": bucket_details = UpdateBucketDetails() bucket_details.compartment_id = module.params["compartment_id"] bucket_details.name = module.params["name"] bucket_details.public_access_type = module.params["public_access_type"] bucket_details.metadata = module.params["metadata"] return bucket_details def filter_resources(all_resources, filter_params): if not filter_params: return all_resources filtered_resources = [] filtered_resources.extend( [ resource for resource in all_resources for key, value in filter_params.items() if getattr(resource, key) == value ] ) return filtered_resources def list_all_resources(target_fn, **kwargs): """ Return all resources after paging through all results returned by target_fn. If a `display_name` or `name` is provided as a kwarg, then only resources matching the specified name are returned. :param target_fn: The target OCI SDK paged function to call :param kwargs: All arguments that the OCI SDK paged function expects :return: List of all objects returned by target_fn :raises ServiceError: When the Service returned an Error response :raises MaximumWaitTimeExceededError: When maximum wait time is exceeded while invoking target_fn """ filter_params = None try: response = call_with_backoff(target_fn, **kwargs) except ValueError as ex: if "unknown kwargs" in str(ex): if "display_name" in kwargs: if kwargs["display_name"]: filter_params = {"display_name": kwargs["display_name"]} del kwargs["display_name"] elif "name" in kwargs: if kwargs["name"]: filter_params = {"name": kwargs["name"]} del kwargs["name"] response = call_with_backoff(target_fn, **kwargs) existing_resources = response.data while response.has_next_page: kwargs.update(page=response.headers.get(HEADER_NEXT_PAGE)) response = call_with_backoff(target_fn, **kwargs) existing_resources += response.data # If the underlying SDK Service list* method doesn't support filtering by name or display_name, filter the resources # and return the matching list of resources return filter_resources(existing_resources, filter_params) def _debug(s): get_logger("oci_utils").debug(s) def get_logger(module_name): oci_logging = setup_logging() return oci_logging.getLogger(module_name) def setup_logging( default_level="INFO", ): """Setup logging configuration""" env_log_path = "LOG_PATH" env_log_level = "LOG_LEVEL" default_log_path = tempfile.gettempdir() log_path = os.getenv(env_log_path, default_log_path) log_level_str = os.getenv(env_log_level, default_level) log_level = logging.getLevelName(log_level_str) log_file_path = os.path.join(log_path, "oci_ansible_module.log") logging.basicConfig(filename=log_file_path, filemode="a", level=log_level) return logging def check_and_update_attributes( target_instance, attr_name, input_value, existing_value, changed ): """ This function checks the difference between two resource attributes of literal types and sets the attrbute value in the target instance type holding the attribute. :param target_instance: The instance which contains the attribute whose values to be compared :param attr_name: Name of the attribute whose value required to be compared :param input_value: The value of the attribute provided by user :param existing_value: The value of the attribute in the existing resource :param changed: Flag to indicate whether there is any difference between the values :return: Returns a boolean value indicating whether there is any difference between the values """ if input_value is not None and not eq(input_value, existing_value): changed = True target_instance.__setattr__(attr_name, input_value) else: target_instance.__setattr__(attr_name, existing_value) return changed def check_and_update_resource( resource_type, get_fn, kwargs_get, update_fn, primitive_params_update, kwargs_non_primitive_update, module, update_attributes, client=None, sub_attributes_of_update_model=None, wait_applicable=True, states=None, ): """ This function handles update operation on a resource. It checks whether update is required and accordingly returns the resource and the changed status. :param wait_applicable: Indicates if the resource support wait :param client: The resource Client class to use to perform the wait checks. This param must be specified if wait_applicable is True :param resource_type: The type of the resource. e.g. "private_ip" :param get_fn: Function used to get the resource. e.g. virtual_network_client.get_private_ip :param kwargs_get: Dictionary containing the arguments to be used to call get function. e.g. {"private_ip_id": module.params["private_ip_id"]} :param update_fn: Function used to update the resource. e.g virtual_network_client.update_private_ip :param primitive_params_update: List of primitive parameters used for update function. e.g. ['private_ip_id'] :param kwargs_non_primitive_update: Dictionary containing the non-primitive arguments to be used to call get function with key as the non-primitive argument type & value as the name of the non-primitive argument to be passed to the update function. e.g. {UpdatePrivateIpDetails: "update_private_ip_details"} :param module: Instance of AnsibleModule :param update_attributes: Attributes in update model. :param states: List of lifecycle states to watch for while waiting after create_fn is called. e.g. [module.params['wait_until'], "FAULTY"] :param sub_attributes_of_update_model: Dictionary of non-primitive sub-attributes of update model. for example, {'services': [ServiceIdRequestDetails()]} as in UpdateServiceGatewayDetails. :return: Returns a dictionary containing the "changed" status and the resource. """ try: result = dict(changed=False) attributes_to_update, resource = get_attr_to_update( get_fn, kwargs_get, module, update_attributes ) if attributes_to_update: kwargs_update = get_kwargs_update( attributes_to_update, kwargs_non_primitive_update, module, primitive_params_update, sub_attributes_of_update_model, ) resource = call_with_backoff(update_fn, **kwargs_update).data if wait_applicable: if client is None: module.fail_json( msg="wait_applicable is True, but client is not specified." ) resource = wait_for_resource_lifecycle_state( client, module, True, kwargs_get, get_fn, None, resource, states ) result["changed"] = True result[resource_type] = to_dict(resource) return result except ServiceError as ex: module.fail_json(msg=ex.message) def get_kwargs_update( attributes_to_update, kwargs_non_primitive_update, module, primitive_params_update, sub_attributes_of_update_model=None, ): kwargs_update = dict() for param in primitive_params_update: kwargs_update[param] = module.params[param] for param in kwargs_non_primitive_update: update_object = param() for key in update_object.attribute_map: if key in attributes_to_update: if ( sub_attributes_of_update_model and key in sub_attributes_of_update_model ): setattr(update_object, key, sub_attributes_of_update_model[key]) else: setattr(update_object, key, module.params[key]) kwargs_update[kwargs_non_primitive_update[param]] = update_object return kwargs_update def is_dictionary_subset(sub, super_dict): """ This function checks if `sub` dictionary is a subset of `super` dictionary. :param sub: subset dictionary, for example user_provided_attr_value. :param super_dict: super dictionary, for example resources_attr_value. :return: True if sub is contained in super. """ for key in sub: if sub[key] != super_dict[key]: return False return True def are_lists_equal(s, t): if s is None and t is None: return True if (s is None and len(t) >= 0) or (t is None and len(s) >= 0) or (len(s) != len(t)): return False if len(s) == 0: return True s = to_dict(s) t = to_dict(t) if type(s[0]) == dict: # Handle list of dicts. Dictionary returned by the API may have additional keys. For example, a get call on # service gateway has an attribute `services` which is a list of `ServiceIdResponseDetails`. This has a key # `service_name` which is not provided in the list of `services` by a user while making an update call; only # `service_id` is provided by the user in the update call. sorted_s = sort_list_of_dictionary(s) sorted_t = sort_list_of_dictionary(t) for index, d in enumerate(sorted_s): if not is_dictionary_subset(d, sorted_t[index]): return False return True else: # Handle lists of primitive types. try: for elem in s: t.remove(elem) except ValueError: return False return not t def get_attr_to_update(get_fn, kwargs_get, module, update_attributes): try: resource = call_with_backoff(get_fn, **kwargs_get).data except ServiceError as ex: module.fail_json(msg=ex.message) attributes_to_update = [] for attr in update_attributes: resources_attr_value = getattr(resource, attr, None) user_provided_attr_value = module.params.get(attr, None) unequal_list_attr = ( type(resources_attr_value) == list or type(user_provided_attr_value) == list ) and not are_lists_equal(user_provided_attr_value, resources_attr_value) unequal_attr = type(resources_attr_value) != list and to_dict( resources_attr_value ) != to_dict(user_provided_attr_value) if unequal_list_attr or unequal_attr: # only update if the user has explicitly provided a value for this attribute # otherwise, no update is necessary because the user hasn't expressed a particular # value for that attribute if module.params.get(attr, None): attributes_to_update.append(attr) return attributes_to_update, resource def get_taggable_arg_spec(supports_create=False, supports_wait=False): """ Returns an arg_spec that is valid for taggable OCI resources. :return: A dict that represents an ansible arg spec that builds over the common_arg_spec and adds free-form and defined tags. """ tag_arg_spec = get_common_arg_spec(supports_create, supports_wait) tag_arg_spec.update( dict(freeform_tags=dict(type="dict"), defined_tags=dict(type="dict")) ) return tag_arg_spec def add_tags_to_model_from_module(model, module): """ Adds free-form and defined tags from an ansible module to a resource model :param model: A resource model instance that supports 'freeform_tags' and 'defined_tags' as attributes :param module: An AnsibleModule representing the options provided by the user :return: The updated model class with the tags specified by the user. """ freeform_tags = module.params.get("freeform_tags", None) defined_tags = module.params.get("defined_tags", None) return add_tags_to_model_class(model, freeform_tags, defined_tags) def add_tags_to_model_class(model, freeform_tags, defined_tags): """ Add free-form and defined tags to a resource model. :param model: A resource model instance that supports 'freeform_tags' and 'defined_tags' as attributes :param freeform_tags: A dict representing the freeform_tags to be applied to the model :param defined_tags: A dict representing the defined_tags to be applied to the model :return: The updated model class with the tags specified by the user """ try: if freeform_tags is not None: _debug("Model {0} set freeform tags to {1}".format(model, freeform_tags)) model.__setattr__("freeform_tags", freeform_tags) if defined_tags is not None: _debug("Model {0} set defined tags to {1}".format(model, defined_tags)) model.__setattr__("defined_tags", defined_tags) except AttributeError as ae: _debug("Model {0} doesn't support tags. Error {1}".format(model, ae)) return model def check_and_create_resource( resource_type, create_fn, kwargs_create, list_fn, kwargs_list, module, model, existing_resources=None, exclude_attributes=None, dead_states=None, default_attribute_values=None, supports_sort_by_time_created=True, ): """ This function checks whether there is a resource with same attributes as specified in the module options. If not, it creates and returns the resource. :param resource_type: Type of the resource to be created. :param create_fn: Function used in the module to handle create operation. The function should return a dict with keys as resource & changed. :param kwargs_create: Dictionary of parameters for create operation. :param list_fn: List function in sdk to list all the resources of type resource_type. :param kwargs_list: Dictionary of parameters for list operation. :param module: Instance of AnsibleModule :param model: Model used to create a resource. :param exclude_attributes: The attributes which should not be used to distinguish the resource. e.g. display_name, dns_label. :param dead_states: List of states which can't transition to any of the usable states of the resource. This deafults to ["TERMINATING", "TERMINATED", "FAULTY", "FAILED", "DELETING", "DELETED", "UNKNOWN_ENUM_VALUE"] :param default_attribute_values: A dictionary containing default values for attributes. :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ if module.params.get("force_create", None): _debug("Force creating {0}".format(resource_type)) result = call_with_backoff(create_fn, **kwargs_create) return result # Get the existing resources list sorted by creation time in descending order. Return the latest matching resource # in case of multiple resource matches. if exclude_attributes is None: exclude_attributes = {} if default_attribute_values is None: default_attribute_values = {} try: if existing_resources is None: if supports_sort_by_time_created: kwargs_list["sort_by"] = "TIMECREATED" existing_resources = list_all_resources(list_fn, **kwargs_list) except ValueError: # list_fn doesn't support sort_by, so remove the sort_by key in kwargs_list and retry kwargs_list.pop("sort_by", None) try: existing_resources = list_all_resources(list_fn, **kwargs_list) # Handle errors like 404 due to bad arguments to the list_all_resources call. except ServiceError as ex: module.fail_json(msg=ex.message) except ServiceError as ex: module.fail_json(msg=ex.message) result = dict() attributes_to_consider = _get_attributes_to_consider( exclude_attributes, model, module ) if "defined_tags" not in default_attribute_values: default_attribute_values["defined_tags"] = {} resource_matched = None _debug( "Trying to find a match within {0} existing resources".format( len(existing_resources) ) ) for resource in existing_resources: if _is_resource_active(resource, dead_states): _debug( "Comparing user specified values {0} against an existing resource's " "values {1}".format(module.params, to_dict(resource)) ) if does_existing_resource_match_user_inputs( to_dict(resource), module, attributes_to_consider, exclude_attributes, default_attribute_values, ): resource_matched = to_dict(resource) break if resource_matched: _debug("Resource with same attributes found: {0}.".format(resource_matched)) result[resource_type] = resource_matched result["changed"] = False else: _debug("No matching resource found. Attempting to create a new resource.") result = call_with_backoff(create_fn, **kwargs_create) return result def _get_attributes_to_consider(exclude_attributes, model, module): """ Determine the attributes to detect if an existing resource already matches the requested resource state :param exclude_attributes: Attributes to not consider for matching :param model: The model class used to create the Resource :param module: An instance of AnsibleModule that contains user's desires around a resource's state :return: A list of attributes that needs to be matched """ # If a user explicitly requests us to match only against a set of resources (using 'key_by', use that as the list # of attributes to consider for matching. if "key_by" in module.params and module.params["key_by"] is not None: attributes_to_consider = module.params["key_by"] else: # Consider all attributes except freeform_tags as freeform tags do not distinguish a resource. attributes_to_consider = list(model.attribute_map) if "freeform_tags" in attributes_to_consider: attributes_to_consider.remove("freeform_tags") # Temporarily removing node_count as the exisiting resource does not reflect it if "node_count" in attributes_to_consider: attributes_to_consider.remove("node_count") _debug("attributes to consider: {0}".format(attributes_to_consider)) return attributes_to_consider def _is_resource_active(resource, dead_states): if dead_states is None: dead_states = DEAD_STATES if "lifecycle_state" not in resource.attribute_map: return True return resource.lifecycle_state not in dead_states def is_attr_assigned_default(default_attribute_values, attr, assigned_value): if not default_attribute_values: return False if attr in default_attribute_values: default_val_for_attr = default_attribute_values.get(attr, None) if isinstance(default_val_for_attr, dict): # When default value for a resource's attribute is empty dictionary, check if the corresponding value of the # existing resource's attribute is also empty. if not default_val_for_attr: return not assigned_value # only compare keys that are in default_attribute_values[attr] # this is to ensure forward compatibility when the API returns new keys that are not known during # the time when the module author provided default values for the attribute keys = {} for k, v in iteritems(assigned_value.items()): if k in default_val_for_attr: keys[k] = v return default_val_for_attr == keys # non-dict, normal comparison return default_val_for_attr == assigned_value else: # module author has not provided a default value for attr return True def create_resource(resource_type, create_fn, kwargs_create, module): """ Create an OCI resource :param resource_type: Type of the resource to be created. e.g.: "vcn" :param create_fn: Function in the SDK to create the resource. e.g. virtual_network_client.create_vcn :param kwargs_create: Dictionary containing arguments to be used to call the create function create_fn :param module: Instance of AnsibleModule """ result = dict(changed=False) try: resource = to_dict(call_with_backoff(create_fn, **kwargs_create).data) _debug("Created {0}, {1}".format(resource_type, resource)) result["changed"] = True result[resource_type] = resource return result except (ServiceError, TypeError) as ex: module.fail_json(msg=str(ex)) def does_existing_resource_match_user_inputs( existing_resource, module, attributes_to_compare, exclude_attributes, default_attribute_values=None, ): """ Check if 'attributes_to_compare' in an existing_resource match the desired state provided by a user in 'module'. :param existing_resource: A dictionary representing an existing resource's values. :param module: The AnsibleModule representing the options provided by the user. :param attributes_to_compare: A list of attributes of a resource that are used to compare if an existing resource matches the desire state of the resource expressed by the user in 'module'. :param exclude_attributes: The attributes, that a module author provides, which should not be used to match the resource. This dictionary typically includes: (a) attributes which are initialized with dynamic default values like 'display_name', 'security_list_ids' for subnets and (b) attributes that don't have any defaults like 'dns_label' in VCNs. The attributes are part of keys and 'True' is the value for all existing keys. :param default_attribute_values: A dictionary containing default values for attributes. :return: True if the values for the list of attributes is the same in the existing_resource and module instances. """ if not default_attribute_values: default_attribute_values = {} for attr in attributes_to_compare: attribute_with_default_metadata = None if attr in existing_resource: resources_value_for_attr = existing_resource[attr] # Check if the user has explicitly provided the value for attr. user_provided_value_for_attr = _get_user_provided_value(module, attr) if user_provided_value_for_attr is not None: res = [True] check_if_user_value_matches_resources_attr( attr, resources_value_for_attr, user_provided_value_for_attr, exclude_attributes, default_attribute_values, res, ) if not res[0]: _debug( "Mismatch on attribute '{0}'. User provided value is {1} & existing resource's value" "is {2}.".format( attr, user_provided_value_for_attr, resources_value_for_attr ) ) return False else: # If the user has not explicitly provided the value for attr and attr is in exclude_list, we can # consider this as a 'pass'. For example, if an attribute 'display_name' is not specified by user and # that attribute is in the 'exclude_list' according to the module author(Not User), then exclude if ( exclude_attributes.get(attr) is None and resources_value_for_attr is not None ): if module.argument_spec.get(attr): attribute_with_default_metadata = module.argument_spec.get(attr) default_attribute_value = attribute_with_default_metadata.get( "default", None ) if default_attribute_value is not None: if existing_resource[attr] != default_attribute_value: return False # Check if attr has a value that is not default. For example, a custom `security_list_id` # is assigned to the subnet's attribute `security_list_ids`. If the attribute is assigned a # value that is not the default, then it must be considered a mismatch and false returned. elif not is_attr_assigned_default( default_attribute_values, attr, existing_resource[attr] ): return False else: _debug( "Attribute {0} is in the create model of resource {1}" "but doesn't exist in the get model of the resource".format( attr, existing_resource.__class__ ) ) return True def tuplize(d): """ This function takes a dictionary and converts it to a list of tuples recursively. :param d: A dictionary. :return: List of tuples. """ list_of_tuples = [] key_list = sorted(list(d.keys())) for key in key_list: if type(d[key]) == list: # Convert a value which is itself a list of dict to a list of tuples. if d[key] and type(d[key][0]) == dict: sub_tuples = [] for sub_dict in d[key]: sub_tuples.append(tuplize(sub_dict)) # To handle comparing two None values, while creating a tuple for a {key: value}, make the first element # in the tuple a boolean `True` if value is None so that attributes with None value are put at last # in the sorted list. list_of_tuples.append((sub_tuples is None, key, sub_tuples)) else: list_of_tuples.append((d[key] is None, key, d[key])) elif type(d[key]) == dict: tupled_value = tuplize(d[key]) list_of_tuples.append((tupled_value is None, key, tupled_value)) else: list_of_tuples.append((d[key] is None, key, d[key])) return list_of_tuples def get_key_for_comparing_dict(d): tuple_form_of_d = tuplize(d) return tuple_form_of_d def sort_dictionary(d): """ This function sorts values of a dictionary recursively. :param d: A dictionary. :return: Dictionary with sorted elements. """ sorted_d = {} for key in d: if type(d[key]) == list: if d[key] and type(d[key][0]) == dict: sorted_value = sort_list_of_dictionary(d[key]) sorted_d[key] = sorted_value else: sorted_d[key] = sorted(d[key]) elif type(d[key]) == dict: sorted_d[key] = sort_dictionary(d[key]) else: sorted_d[key] = d[key] return sorted_d def sort_list_of_dictionary(list_of_dict): """ This functions sorts a list of dictionaries. It first sorts each value of the dictionary and then sorts the list of individually sorted dictionaries. For sorting, each dictionary's tuple equivalent is used. :param list_of_dict: List of dictionaries. :return: A sorted dictionary. """ list_with_sorted_dict = [] for d in list_of_dict: sorted_d = sort_dictionary(d) list_with_sorted_dict.append(sorted_d) return sorted(list_with_sorted_dict, key=get_key_for_comparing_dict) def check_if_user_value_matches_resources_attr( attribute_name, resources_value_for_attr, user_provided_value_for_attr, exclude_attributes, default_attribute_values, res, ): if isinstance(default_attribute_values.get(attribute_name), dict): default_attribute_values = default_attribute_values.get(attribute_name) if isinstance(exclude_attributes.get(attribute_name), dict): exclude_attributes = exclude_attributes.get(attribute_name) if isinstance(resources_value_for_attr, list) or isinstance( user_provided_value_for_attr, list ): # Perform a deep equivalence check for a List attribute if exclude_attributes.get(attribute_name): return if ( user_provided_value_for_attr is None and default_attribute_values.get(attribute_name) is not None ): user_provided_value_for_attr = default_attribute_values.get(attribute_name) if resources_value_for_attr is None and user_provided_value_for_attr is None: return if ( resources_value_for_attr is None and len(user_provided_value_for_attr) >= 0 or user_provided_value_for_attr is None and len(resources_value_for_attr) >= 0 ): res[0] = False return if ( resources_value_for_attr is not None and user_provided_value_for_attr is not None and len(resources_value_for_attr) != len(user_provided_value_for_attr) ): res[0] = False return if ( user_provided_value_for_attr and type(user_provided_value_for_attr[0]) == dict ): # Process a list of dict sorted_user_provided_value_for_attr = sort_list_of_dictionary( user_provided_value_for_attr ) sorted_resources_value_for_attr = sort_list_of_dictionary( resources_value_for_attr ) else: sorted_user_provided_value_for_attr = sorted(user_provided_value_for_attr) sorted_resources_value_for_attr = sorted(resources_value_for_attr) # Walk through the sorted list values of the resource's value for this attribute, and compare against user # provided values. for index, resources_value_for_attr_part in enumerate( sorted_resources_value_for_attr ): check_if_user_value_matches_resources_attr( attribute_name, resources_value_for_attr_part, sorted_user_provided_value_for_attr[index], exclude_attributes, default_attribute_values, res, ) elif isinstance(resources_value_for_attr, dict): # Perform a deep equivalence check for dict typed attributes if not resources_value_for_attr and user_provided_value_for_attr: res[0] = False for key in resources_value_for_attr: if ( user_provided_value_for_attr is not None and user_provided_value_for_attr ): check_if_user_value_matches_resources_attr( key, resources_value_for_attr.get(key), user_provided_value_for_attr.get(key), exclude_attributes, default_attribute_values, res, ) else: if exclude_attributes.get(key) is None: if default_attribute_values.get(key) is not None: user_provided_value_for_attr = default_attribute_values.get(key) check_if_user_value_matches_resources_attr( key, resources_value_for_attr.get(key), user_provided_value_for_attr, exclude_attributes, default_attribute_values, res, ) else: res[0] = is_attr_assigned_default( default_attribute_values, attribute_name, resources_value_for_attr.get(key), ) elif resources_value_for_attr != user_provided_value_for_attr: if ( exclude_attributes.get(attribute_name) is None and default_attribute_values.get(attribute_name) is not None ): # As the user has not specified a value for an optional attribute, if the existing resource's # current state has a DEFAULT value for that attribute, we must not consider this incongruence # an issue and continue with other checks. If the existing resource's value for the attribute # is not the default value, then the existing resource is not a match. if not is_attr_assigned_default( default_attribute_values, attribute_name, resources_value_for_attr ): res[0] = False elif user_provided_value_for_attr is not None: res[0] = False def are_dicts_equal( option_name, existing_resource_dict, user_provided_dict, exclude_list, default_attribute_values, ): if not user_provided_dict: # User has not provided a value for the map option. In this case, the user hasn't expressed an intent around # this optional attribute. Check if existing_resource_dict matches default. # For example, source_details attribute in volume is optional and does not have any defaults. return is_attr_assigned_default( default_attribute_values, option_name, existing_resource_dict ) # If the existing resource has an empty dict, while the user has provided entries, dicts are not equal if not existing_resource_dict and user_provided_dict: return False # check if all keys of an existing resource's dict attribute matches user-provided dict's entries for sub_attr in existing_resource_dict: # If user has provided value for sub-attribute, then compare it with corresponding key in existing resource. if sub_attr in user_provided_dict: if existing_resource_dict[sub_attr] != user_provided_dict[sub_attr]: _debug( "Failed to match: Existing resource's attr {0} sub-attr {1} value is {2}, while user " "provided value is {3}".format( option_name, sub_attr, existing_resource_dict[sub_attr], user_provided_dict.get(sub_attr, None), ) ) return False # If sub_attr not provided by user, check if the sub-attribute value of existing resource matches default value. else: if not should_dict_attr_be_excluded(option_name, sub_attr, exclude_list): default_value_for_dict_attr = default_attribute_values.get( option_name, None ) if default_value_for_dict_attr: # if a default value for the sub-attr was provided by the module author, fail if the existing # resource's value for the sub-attr is not the default if not is_attr_assigned_default( default_value_for_dict_attr, sub_attr, existing_resource_dict[sub_attr], ): return False else: # No default value specified by module author for sub_attr _debug( "Consider as match: Existing resource's attr {0} sub-attr {1} value is {2}, while user did" "not provide a value for it. The module author also has not provided a default value for it" "or marked it for exclusion. So ignoring this attribute during matching and continuing with" "other checks".format( option_name, sub_attr, existing_resource_dict[sub_attr] ) ) return True def should_dict_attr_be_excluded(map_option_name, option_key, exclude_list): """An entry for the Exclude list for excluding a map's key is specifed as a dict with the map option name as the key, and the value as a list of keys to be excluded within that map. For example, if the keys "k1" and "k2" of a map option named "m1" needs to be excluded, the exclude list must have an entry {'m1': ['k1','k2']} """ for exclude_item in exclude_list: if isinstance(exclude_item, dict): if map_option_name in exclude_item: if option_key in exclude_item[map_option_name]: return True return False def create_and_wait( resource_type, client, create_fn, kwargs_create, get_fn, get_param, module, states=None, wait_applicable=True, kwargs_get=None, ): """ A utility function to create a resource and wait for the resource to get into the state as specified in the module options. :param wait_applicable: Specifies if wait for create is applicable for this resource :param resource_type: Type of the resource to be created. e.g. "vcn" :param client: OCI service client instance to call the service periodically to retrieve data. e.g. VirtualNetworkClient() :param create_fn: Function in the SDK to create the resource. e.g. virtual_network_client.create_vcn :param kwargs_create: Dictionary containing arguments to be used to call the create function create_fn. :param get_fn: Function in the SDK to get the resource. e.g. virtual_network_client.get_vcn :param get_param: Name of the argument in the SDK get function. e.g. "vcn_id" :param module: Instance of AnsibleModule. :param states: List of lifecycle states to watch for while waiting after create_fn is called. e.g. [module.params['wait_until'], "FAULTY"] :param kwargs_get: Dictionary containing arguments to be used to call a multi-argument `get` function :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ try: return create_or_update_resource_and_wait( resource_type, create_fn, kwargs_create, module, wait_applicable, get_fn, get_param, states, client, kwargs_get, ) except MaximumWaitTimeExceeded as ex: module.fail_json(msg=str(ex)) except ServiceError as ex: module.fail_json(msg=ex.message) def update_and_wait( resource_type, client, update_fn, kwargs_update, get_fn, get_param, module, states=None, wait_applicable=True, kwargs_get=None, ): """ A utility function to update a resource and wait for the resource to get into the state as specified in the module options. It wraps the create_and_wait method as apart from the method and arguments, everything else is similar. :param wait_applicable: Specifies if wait for create is applicable for this resource :param resource_type: Type of the resource to be created. e.g. "vcn" :param client: OCI service client instance to call the service periodically to retrieve data. e.g. VirtualNetworkClient() :param update_fn: Function in the SDK to update the resource. e.g. virtual_network_client.update_vcn :param kwargs_update: Dictionary containing arguments to be used to call the update function update_fn. :param get_fn: Function in the SDK to get the resource. e.g. virtual_network_client.get_vcn :param get_param: Name of the argument in the SDK get function. e.g. "vcn_id" :param module: Instance of AnsibleModule. :param kwargs_get: Dictionary containing arguments to be used to call the get function which requires multiple arguments. :param states: List of lifecycle states to watch for while waiting after update_fn is called. e.g. [module.params['wait_until'], "FAULTY"] :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ try: return create_or_update_resource_and_wait( resource_type, update_fn, kwargs_update, module, wait_applicable, get_fn, get_param, states, client, kwargs_get=kwargs_get, ) except MaximumWaitTimeExceeded as ex: module.fail_json(msg=str(ex)) except ServiceError as ex: module.fail_json(msg=ex.message) def create_or_update_resource_and_wait( resource_type, function, kwargs_function, module, wait_applicable, get_fn, get_param, states, client, update_target_resource_id_in_get_param=False, kwargs_get=None, ): """ A utility function to create or update a resource and wait for the resource to get into the state as specified in the module options. :param resource_type: Type of the resource to be created. e.g. "vcn" :param function: Function in the SDK to create or update the resource. :param kwargs_function: Dictionary containing arguments to be used to call the create or update function :param module: Instance of AnsibleModule. :param wait_applicable: Specifies if wait for create is applicable for this resource :param get_fn: Function in the SDK to get the resource. e.g. virtual_network_client.get_vcn :param get_param: Name of the argument in the SDK get function. e.g. "vcn_id" :param states: List of lifecycle states to watch for while waiting after create_fn is called. e.g. [module.params['wait_until'], "FAULTY"] :param client: OCI service client instance to call the service periodically to retrieve data. e.g. VirtualNetworkClient() :param kwargs_get: Dictionary containing arguments to be used to call the get function which requires multiple arguments. :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ result = create_resource(resource_type, function, kwargs_function, module) resource = result[resource_type] result[resource_type] = wait_for_resource_lifecycle_state( client, module, wait_applicable, kwargs_get, get_fn, get_param, resource, states, resource_type, ) return result def wait_for_resource_lifecycle_state( client, module, wait_applicable, kwargs_get, get_fn, get_param, resource, states, resource_type=None, ): """ A utility function to wait for the resource to get into the state as specified in the module options. :param client: OCI service client instance to call the service periodically to retrieve data. e.g. VirtualNetworkClient :param module: Instance of AnsibleModule. :param wait_applicable: Specifies if wait for create is applicable for this resource :param kwargs_get: Dictionary containing arguments to be used to call the get function which requires multiple arguments. :param get_fn: Function in the SDK to get the resource. e.g. virtual_network_client.get_vcn :param get_param: Name of the argument in the SDK get function. e.g. "vcn_id" :param resource_type: Type of the resource to be created. e.g. "vcn" :param states: List of lifecycle states to watch for while waiting after create_fn is called. e.g. [module.params['wait_until'], "FAULTY"] :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ if wait_applicable and module.params.get("wait", None): if resource_type == "compartment": # An immediate attempt to retrieve a compartment after a compartment is created fails with # 'Authorization failed or requested resource not found', 'status': 404}. # This is because it takes few seconds for the permissions on a compartment to be ready. # Wait for few seconds before attempting a get call on compartment. _debug( "Pausing execution for permission on the newly created compartment to be ready." ) time.sleep(15) if kwargs_get: _debug( "Waiting for resource to reach READY state. get_args: {0}".format( kwargs_get ) ) response_get = call_with_backoff(get_fn, **kwargs_get) else: _debug( "Waiting for resource with id {0} to reach READY state.".format( resource["id"] ) ) response_get = call_with_backoff(get_fn, **{get_param: resource["id"]}) if states is None: states = module.params.get("wait_until") or DEFAULT_READY_STATES resource = to_dict( oci.wait_until( client, response_get, evaluate_response=lambda r: r.data.lifecycle_state in states, max_wait_seconds=module.params.get( "wait_timeout", MAX_WAIT_TIMEOUT_IN_SECONDS ), ).data ) return resource def wait_on_work_request(client, response, module): try: if module.params.get("wait", None): _debug( "Waiting for work request with id {0} to reach SUCCEEDED state.".format( response.data.id ) ) wait_response = oci.wait_until( client, response, evaluate_response=lambda r: r.data.status == "SUCCEEDED", max_wait_seconds=module.params.get( "wait_timeout", MAX_WAIT_TIMEOUT_IN_SECONDS ), ) else: _debug( "Waiting for work request with id {0} to reach ACCEPTED state.".format( response.data.id ) ) wait_response = oci.wait_until( client, response, evaluate_response=lambda r: r.data.status == "ACCEPTED", max_wait_seconds=module.params.get( "wait_timeout", MAX_WAIT_TIMEOUT_IN_SECONDS ), ) except MaximumWaitTimeExceeded as ex: _debug(str(ex)) module.fail_json(msg=str(ex)) except ServiceError as ex: _debug(str(ex)) module.fail_json(msg=str(ex)) return wait_response.data def delete_and_wait( resource_type, client, get_fn, kwargs_get, delete_fn, kwargs_delete, module, states=None, wait_applicable=True, process_work_request=False, ): """A utility function to delete a resource and wait for the resource to get into the state as specified in the module options. :param wait_applicable: Specifies if wait for delete is applicable for this resource :param resource_type: Type of the resource to be deleted. e.g. "vcn" :param client: OCI service client instance to call the service periodically to retrieve data. e.g. VirtualNetworkClient() :param get_fn: Function in the SDK to get the resource. e.g. virtual_network_client.get_vcn :param kwargs_get: Dictionary of arguments for get function get_fn. e.g. {"vcn_id": module.params["id"]} :param delete_fn: Function in the SDK to delete the resource. e.g. virtual_network_client.delete_vcn :param kwargs_delete: Dictionary of arguments for delete function delete_fn. e.g. {"vcn_id": module.params["id"]} :param module: Instance of AnsibleModule. :param states: List of lifecycle states to watch for while waiting after delete_fn is called. If nothing is passed, defaults to ["TERMINATED", "DETACHED", "DELETED"]. :param process_work_request: Whether a work request is generated on an API call and if it needs to be handled. :return: A dictionary containing the resource & the "changed" status. e.g. {"vcn":{x:y}, "changed":True} """ states_set = set(["DETACHING", "DETACHED", "DELETING", "DELETED", "TERMINATING", "TERMINATED"]) result = dict(changed=False) result[resource_type] = dict() try: resource = to_dict(call_with_backoff(get_fn, **kwargs_get).data) if resource: if "lifecycle_state" not in resource or resource["lifecycle_state"] not in states_set: response = call_with_backoff(delete_fn, **kwargs_delete) if process_work_request: wr_id = response.headers.get("opc-work-request-id") get_wr_response = call_with_backoff( client.get_work_request, work_request_id=wr_id ) result["work_request"] = to_dict( wait_on_work_request(client, get_wr_response, module) ) # Set changed to True as work request has been created to delete the resource. result["changed"] = True resource = to_dict(call_with_backoff(get_fn, **kwargs_get).data) else: _debug("Deleted {0}, {1}".format(resource_type, resource)) result["changed"] = True if wait_applicable and module.params.get("wait", None): if states is None: states = ( module.params.get("wait_until") or DEFAULT_TERMINATED_STATES ) try: wait_response = oci.wait_until( client, get_fn(**kwargs_get), evaluate_response=lambda r: r.data.lifecycle_state in states, max_wait_seconds=module.params.get( "wait_timeout", MAX_WAIT_TIMEOUT_IN_SECONDS ), succeed_on_not_found=True, ) except MaximumWaitTimeExceeded as ex: module.fail_json(msg=str(ex)) except ServiceError as ex: if ex.status != 404: module.fail_json(msg=ex.message) else: # While waiting for resource to get into terminated state, if the resource is not found. _debug( "API returned Status:404(Not Found) while waiting for resource to get into" " terminated state." ) resource["lifecycle_state"] = "DELETED" result[resource_type] = resource return result # oci.wait_until() returns an instance of oci.util.Sentinel in case the resource is not found. if type(wait_response) is not Sentinel: resource = to_dict(wait_response.data) else: resource["lifecycle_state"] = "DELETED" result[resource_type] = resource else: _debug( "Resource {0} with {1} already deleted. So returning changed=False".format( resource_type, kwargs_get ) ) except ServiceError as ex: # DNS API throws a 400 InvalidParameter when a zone id is provided for zone_name_or_id and if the zone # resource is not available, instead of the expected 404. So working around this for now. if type(client) == oci.dns.DnsClient: if ex.status == 400 and ex.code == "InvalidParameter": _debug( "Resource {0} with {1} already deleted. So returning changed=False".format( resource_type, kwargs_get ) ) elif ex.status != 404: module.fail_json(msg=ex.message) result[resource_type] = dict() return result def are_attrs_equal(current_resource, module, attributes): """ Check if the specified attributes are equal in the specified 'model' and 'module'. This is used to check if an OCI Model instance already has the values specified by an Ansible user while invoking an OCI Ansible module and if a resource needs to be updated. :param current_resource: A resource model instance :param module: The AnsibleModule representing the options provided by the user :param attributes: A list of attributes that would need to be compared in the model and the module instances. :return: True if the values for the list of attributes is the same in the model and module instances """ for attr in attributes: curr_value = getattr(current_resource, attr, None) user_provided_value = _get_user_provided_value(module, attribute_name=attr) if user_provided_value is not None: if curr_value != user_provided_value: _debug( "are_attrs_equal - current resource's attribute " + attr + " value is " + str(curr_value) + " and this doesn't match user provided value of " + str(user_provided_value) ) return False return True def _get_user_provided_value(module, attribute_name): """ Returns the user provided value for "attribute_name". We consider aliases in the module. """ user_provided_value = module.params.get(attribute_name, None) if user_provided_value is None: # If the attribute_name is set as an alias for some option X and user has provided value in the playbook using # option X, then user provided value for attribute_name is equal to value for X. # Get option name for attribute_name from module.aliases. # module.aliases is a dictionary with key as alias name and its value as option name. option_alias_for_attribute = module.aliases.get(attribute_name, None) if option_alias_for_attribute is not None: user_provided_value = module.params.get(option_alias_for_attribute, None) return user_provided_value def update_model_with_user_options(curr_model, update_model, module): """ Update the 'update_model' with user provided values in 'module' for the specified 'attributes' if they are different from the values in the 'curr_model'. :param curr_model: A resource model instance representing the state of the current resource :param update_model: An instance of the update resource model for the current resource's type :param module: An AnsibleModule representing the options provided by the user :return: An updated 'update_model' instance filled with values that would need to be updated in the current resource state to satisfy the user's requested state. """ attributes = update_model.attribute_map.keys() for attr in attributes: curr_value_for_attr = getattr(curr_model, attr, None) user_provided_value = _get_user_provided_value(module, attribute_name=attr) if curr_value_for_attr != user_provided_value: if user_provided_value is not None: # Only update if a user has specified a value for an option _debug( "User requested {0} for attribute {1}, whereas the current value is {2}. So adding it " "to the update model".format( user_provided_value, attr, curr_value_for_attr ) ) setattr(update_model, attr, user_provided_value) else: # Always set current values of the resource in the update model if there is no request for change in # values setattr(update_model, attr, curr_value_for_attr) return update_model def _get_retry_strategy(): retry_strategy_builder = RetryStrategyBuilder( max_attempts_check=True, max_attempts=10, retry_max_wait_between_calls_seconds=30, retry_base_sleep_time_seconds=3, backoff_type=oci.retry.BACKOFF_FULL_JITTER_EQUAL_ON_THROTTLE_VALUE, ) retry_strategy_builder.add_service_error_check( service_error_retry_config={ 429: [], 400: ["QuotaExceeded", "LimitExceeded"], 409: ["Conflict"], }, service_error_retry_on_any_5xx=True, ) return retry_strategy_builder.get_retry_strategy() def call_with_backoff(fn, **kwargs): if "retry_strategy" not in kwargs: kwargs["retry_strategy"] = _get_retry_strategy() try: return fn(**kwargs) except TypeError as te: if "unexpected keyword argument" in str(te): # to handle older SDKs that did not support retry_strategy del kwargs["retry_strategy"] return fn(**kwargs) else: # A validation error raised by the SDK, throw it back raise def generic_hash(obj): """ Compute a hash of all the fields in the object :param obj: Object whose hash needs to be computed :return: a hash value for the object """ sum = 0 for field in obj.attribute_map.keys(): field_value = getattr(obj, field) if isinstance(field_value, list): for value in field_value: sum = sum + hash(value) elif isinstance(field_value, dict): for k, v in field_value.items(): sum = sum + hash(hash(k) + hash(":") + hash(v)) else: sum = sum + hash(getattr(obj, field)) return sum def generic_eq(s, other): if other is None: return False return s.__dict__ == other.__dict__ def generate_subclass(parent_class): """Make a class hash-able by generating a subclass with a __hash__ method that returns the sum of all fields within the parent class""" dict_of_method_in_subclass = { "__init__": parent_class.__init__, "__hash__": generic_hash, "__eq__": generic_eq, } subclass_name = "GeneratedSub" + parent_class.__name__ generated_sub_class = type( subclass_name, (parent_class,), dict_of_method_in_subclass ) return generated_sub_class def create_hashed_instance(class_type): hashed_class = generate_subclass(class_type) return hashed_class() def get_hashed_object_list(class_type, object_with_values, attributes_class_type=None): if object_with_values is None: return None hashed_class_instances = [] for object_with_value in object_with_values: hashed_class_instances.append( get_hashed_object(class_type, object_with_value, attributes_class_type) ) return hashed_class_instances def get_hashed_object( class_type, object_with_value, attributes_class_type=None, supported_attributes=None ): """ Convert any class instance into hashable so that the instances are eligible for various comparison operation available under set() object. :param class_type: Any class type whose instances needs to be hashable :param object_with_value: Instance of the class type with values which would be set in the resulting isinstance :param attributes_class_type: A list of class types of attributes, if attribute is a custom class instance :param supported_attributes: A list of attributes which should be considered while populating the instance with the values in the object. This helps in avoiding new attributes of the class_type which are still not supported by the current implementation. :return: A hashable instance with same state of the provided object_with_value """ if object_with_value is None: return None HashedClass = generate_subclass(class_type) hashed_class_instance = HashedClass() if supported_attributes: class_attributes = list( set(hashed_class_instance.attribute_map) & set(supported_attributes) ) else: class_attributes = hashed_class_instance.attribute_map for attribute in class_attributes: attribute_value = getattr(object_with_value, attribute) if attributes_class_type: for attribute_class_type in attributes_class_type: if isinstance(attribute_value, attribute_class_type): attribute_value = get_hashed_object( attribute_class_type, attribute_value ) hashed_class_instance.__setattr__(attribute, attribute_value) return hashed_class_instance def update_class_type_attr_difference( update_class_details, existing_instance, attr_name, attr_class, input_attr_value ): """ Checks the difference and updates an attribute which is represented by a class instance. Not aplicable if the attribute type is a primitive value. For example, if a class name is A with an attribute x, then if A.x = X(), then only this method works. :param update_class_details The instance which should be updated if there is change in attribute value :param existing_instance The instance whose attribute value is compared with input attribute value :param attr_name Name of the attribute whose value should be compared :param attr_class Class type of the attribute :param input_attr_value The value of input attribute which should replaced the current value in case of mismatch :return: A boolean value indicating whether attribute value has been replaced """ changed = False # Here existing attribute values is an instance existing_attr_value = get_hashed_object( attr_class, getattr(existing_instance, attr_name) ) if input_attr_value is None: update_class_details.__setattr__(attr_name, existing_attr_value) else: changed = not input_attr_value.__eq__(existing_attr_value) if changed: update_class_details.__setattr__(attr_name, input_attr_value) else: update_class_details.__setattr__(attr_name, existing_attr_value) return changed def get_existing_resource(target_fn, module, **kwargs): """ Returns the requested resource if it exists based on the input arguments. :param target_fn The function which should be used to find the requested resource :param module Instance of AnsibleModule attribute value :param kwargs A map of arguments consisting of values based on which requested resource should be searched :return: Instance of requested resource """ existing_resource = None try: response = call_with_backoff(target_fn, **kwargs) existing_resource = response.data except ServiceError as ex: if ex.status != 404: module.fail_json(msg=ex.message) return existing_resource def get_attached_instance_info( module, lookup_attached_instance, list_attachments_fn, list_attachments_args ): config = get_oci_config(module) identity_client = create_service_client(module, IdentityClient) volume_attachments = [] if lookup_attached_instance: # Get all the compartments in the tenancy compartments = to_dict( identity_client.list_compartments( config.get("tenancy"), compartment_id_in_subtree=True ).data ) # For each compartment, get the volume attachments for the compartment_id with the other args in # list_attachments_args. for compartment in compartments: list_attachments_args["compartment_id"] = compartment["id"] try: volume_attachments += list_all_resources( list_attachments_fn, **list_attachments_args ) # Pass ServiceError due to authorization issue in accessing volume attachments of a compartment except ServiceError as ex: if ex.status == 404: pass else: volume_attachments = list_all_resources( list_attachments_fn, **list_attachments_args ) volume_attachments = to_dict(volume_attachments) # volume_attachments has attachments in DETACHING or DETACHED state. Return the volume attachment in ATTACHING or # ATTACHED state return next( ( volume_attachment for volume_attachment in volume_attachments if volume_attachment["lifecycle_state"] in ["ATTACHING", "ATTACHED"] ), None, ) def check_mode(fn): def wrapper(*args, **kwargs): if os.environ.get("OCI_ANSIBLE_EXPERIMENTAL", None): return fn(*args, **kwargs) return None return wrapper def check_and_return_component_list_difference( input_component_list, existing_components, purge_components, delete_components=False ): if input_component_list: existing_components, changed = get_component_list_difference( input_component_list, existing_components, purge_components, delete_components, ) else: existing_components = [] changed = True return existing_components, changed def get_component_list_difference( input_component_list, existing_components, purge_components, delete_components=False ): if delete_components: if existing_components is None: return None, False component_differences = set(existing_components).intersection( set(input_component_list) ) if component_differences: return list(set(existing_components) - component_differences), True else: return None, False if existing_components is None: return input_component_list, True if purge_components: components_differences = set(input_component_list).symmetric_difference( set(existing_components) ) if components_differences: return input_component_list, True components_differences = set(input_component_list).difference( set(existing_components) ) if components_differences: return list(components_differences) + existing_components, True return None, False def write_to_file(path, content): with open(to_bytes(path), "wb") as dest_file: dest_file.write(content) def get_target_resource_from_list( module, list_resource_fn, target_resource_id=None, **kwargs ): """ Returns a resource filtered by identifer from a list of resources. This method should be used as an alternative of 'get resource' method when 'get resource' is nor provided by resource api. This method returns a wrapper of response object but that should not be used as an input to 'wait_until' utility as this is only a partial wrapper of response object. :param module The AnsibleModule representing the options provided by the user :param list_resource_fn The function which lists all the resources :param target_resource_id The identifier of the resource which should be filtered from the list :param kwargs A map of arguments consisting of values based on which requested resource should be searched :return: A custom wrapper which partially wraps a response object where the data field contains the target resource, if found. """ class ResponseWrapper: def __init__(self, data): self.data = data try: resources = list_all_resources(list_resource_fn, **kwargs) if resources is not None: for resource in resources: if resource.id == target_resource_id: # Returning an object that mimics an OCI response as oci_utils methods assumes an Response-ish # object return ResponseWrapper(data=resource) return ResponseWrapper(data=None) except ServiceError as ex: module.fail_json(msg=ex.message)