def compare_list_of_dicts(old, new, convert_id_to_name=None): ''' Compare lists of dictionaries representing Azure objects. Only keys found in the "new" dictionaries are compared to the "old" dictionaries, since getting Azure objects from the API returns some read-only data which should not be used in the comparison. A list of parameter names can be passed in order to compare a bare object name to a full Azure ID path for brevity. If string types are found in values, comparison is case insensitive. Return comment should be used to trigger exit from the calling function. ''' ret = {} if not convert_id_to_name: convert_id_to_name = [] if not isinstance(new, list): ret['comment'] = 'must be provided as a list of dictionaries!' return ret if len(new) != len(old): ret['changes'] = { 'old': old, 'new': new } return ret try: local_configs, remote_configs = [sorted(config, key=itemgetter('name')) for config in (new, old)] except TypeError: ret['comment'] = 'configurations must be provided as a list of dictionaries!' return ret except KeyError: ret['comment'] = 'configuration dictionaries must contain the "name" key!' return ret for idx in six_range(0, len(local_configs)): for key in local_configs[idx]: local_val = local_configs[idx][key] if key in convert_id_to_name: remote_val = remote_configs[idx].get(key, {}).get('id', '').split('/')[-1] else: remote_val = remote_configs[idx].get(key) if isinstance(local_val, six.string_types): local_val = local_val.lower() if isinstance(remote_val, six.string_types): remote_val = remote_val.lower() if local_val != remote_val: ret['changes'] = { 'old': remote_configs, 'new': local_configs } return ret return ret
def record_set_present( name, zone_name, resource_group, record_type, if_match=None, if_none_match=None, etag=None, metadata=None, ttl=None, arecords=None, aaaa_records=None, mx_records=None, ns_records=None, ptr_records=None, srv_records=None, txt_records=None, cname_record=None, soa_record=None, caa_records=None, connection_auth=None, **kwargs ): """ .. versionadded:: 3000 Ensure a record set exists in a DNS zone. :param name: The name of the record set, relative to the name of the zone. :param zone_name: Name of the DNS zone (without a terminating dot). :param resource_group: The resource group assigned to the DNS zone. :param record_type: The type of DNS record in this record set. Record sets of type SOA can be updated but not created (they are created when the DNS zone is created). Possible values include: 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT' :param if_match: The etag of the record set. Omit this value to always overwrite the current record set. Specify the last-seen etag value to prevent accidentally overwritting any concurrent changes. :param if_none_match: Set to '*' to allow a new record set to be created, but to prevent updating an existing record set. Other values will be ignored. :param etag: The etag of the record set. `Etags <https://docs.microsoft.com/en-us/azure/dns/dns-zones-records#etags>`__ are used to handle concurrent changes to the same resource safely. :param metadata: A dictionary of strings can be passed as tag metadata to the record set object. :param ttl: The TTL (time-to-live) of the records in the record set. Required when specifying record information. :param arecords: The list of A records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.arecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param aaaa_records: The list of AAAA records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.aaaarecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param mx_records: The list of MX records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.mxrecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param ns_records: The list of NS records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.nsrecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param ptr_records: The list of PTR records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.ptrrecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param srv_records: The list of SRV records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.srvrecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param txt_records: The list of TXT records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.txtrecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param cname_record: The CNAME record in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.cnamerecord?view=azure-python>`__ to create a dictionary representing the record object. :param soa_record: The SOA record in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.soarecord?view=azure-python>`__ to create a dictionary representing the record object. :param caa_records: The list of CAA records in the record set. View the `Azure SDK documentation <https://docs.microsoft.com/en-us/python/api/azure.mgmt.dns.models.caarecord?view=azure-python>`__ to create a list of dictionaries representing the record objects. :param connection_auth: A dict with subscription and authentication parameters to be used in connecting to the Azure Resource Manager API. Example usage: .. code-block:: yaml Ensure record set exists: azurearm_dns.record_set_present: - name: web - zone_name: contoso.com - resource_group: my_rg - record_type: A - ttl: 300 - arecords: - ipv4_address: 10.0.0.1 - metadata: how_awesome: very contact_name: Elmer Fudd Gantry - connection_auth: {{ profile }} """ ret = {"name": name, "result": False, "comment": "", "changes": {}} record_vars = [ "arecords", "aaaa_records", "mx_records", "ns_records", "ptr_records", "srv_records", "txt_records", "cname_record", "soa_record", "caa_records", ] if not isinstance(connection_auth, dict): ret[ "comment" ] = "Connection information must be specified via connection_auth dictionary!" return ret rec_set = __salt__["azurearm_dns.record_set_get"]( name, zone_name, resource_group, record_type, azurearm_log_level="info", **connection_auth ) if "error" not in rec_set: metadata_changes = __utils__["dictdiffer.deep_diff"]( rec_set.get("metadata", {}), metadata or {} ) if metadata_changes: ret["changes"]["metadata"] = metadata_changes for record_str in record_vars: # pylint: disable=eval-used record = eval(record_str) if record: if not ttl: ret[ "comment" ] = "TTL is required when specifying record information!" return ret if not rec_set.get(record_str): ret["changes"] = {"new": {record_str: record}} continue if record_str[-1] != "s": if not isinstance(record, dict): ret[ "comment" ] = "{0} record information must be specified as a dictionary!".format( record_str ) return ret for k, v in record.items(): if v != rec_set[record_str].get(k): ret["changes"] = {"new": {record_str: record}} elif record_str[-1] == "s": if not isinstance(record, list): ret[ "comment" ] = "{0} record information must be specified as a list of dictionaries!".format( record_str ) return ret local, remote = [ sorted(config) for config in (record, rec_set[record_str]) ] for idx in six_range(0, len(local)): for key in local[idx]: local_val = local[idx][key] remote_val = remote[idx].get(key) if isinstance(local_val, six.string_types): local_val = local_val.lower() if isinstance(remote_val, six.string_types): remote_val = remote_val.lower() if local_val != remote_val: ret["changes"] = {"new": {record_str: record}} if not ret["changes"]: ret["result"] = True ret["comment"] = "Record set {0} is already present.".format(name) return ret if __opts__["test"]: ret["result"] = None ret["comment"] = "Record set {0} would be updated.".format(name) return ret else: ret["changes"] = { "old": {}, "new": { "name": name, "zone_name": zone_name, "resource_group": resource_group, "record_type": record_type, "etag": etag, "metadata": metadata, "ttl": ttl, }, } for record in record_vars: # pylint: disable=eval-used if eval(record): # pylint: disable=eval-used ret["changes"]["new"][record] = eval(record) if __opts__["test"]: ret["comment"] = "Record set {0} would be created.".format(name) ret["result"] = None return ret rec_set_kwargs = kwargs.copy() rec_set_kwargs.update(connection_auth) rec_set = __salt__["azurearm_dns.record_set_create_or_update"]( name=name, zone_name=zone_name, resource_group=resource_group, record_type=record_type, if_match=if_match, if_none_match=if_none_match, etag=etag, ttl=ttl, metadata=metadata, arecords=arecords, aaaa_records=aaaa_records, mx_records=mx_records, ns_records=ns_records, ptr_records=ptr_records, srv_records=srv_records, txt_records=txt_records, cname_record=cname_record, soa_record=soa_record, caa_records=caa_records, **rec_set_kwargs ) if "error" not in rec_set: ret["result"] = True ret["comment"] = "Record set {0} has been created.".format(name) return ret ret["comment"] = "Failed to create record set {0}! ({1})".format( name, rec_set.get("error") ) return ret
def get_client(client_type, **kwargs): """ Dynamically load the selected client and return a management client object ''' client_map = {'compute': 'ComputeManagement', 'authorization': 'AuthorizationManagement', 'dns': 'DnsManagement', 'storage': 'StorageManagement', 'managementlock': 'ManagementLock', 'monitor': 'MonitorManagement', 'network': 'NetworkManagement', 'policy': 'Policy', 'resource': 'ResourceManagement', 'subscription': 'Subscription', 'web': 'WebSiteManagement'} if client_type not in client_map: raise SaltSystemExit( msg='The Azure ARM client_type {0} specified can not be found.'.format( client_type) ) map_value = client_map[client_type] if client_type in ['policy', 'subscription']: module_name = 'resource' elif client_type in ['managementlock']: module_name = 'resource.locks' else: module_name = client_type try: client_module = importlib.import_module("azure.mgmt." + module_name) # pylint: disable=invalid-name Client = getattr(client_module, "{0}Client".format(map_value)) except ImportError: raise sys.exit("The azure {0} client is not available.".format(client_type)) credentials, subscription_id, cloud_env = _determine_auth(**kwargs) if client_type == 'subscription': client = Client( credentials=credentials, base_url=cloud_env.endpoints.resource_manager, ) else: client = Client( credentials=credentials, subscription_id=subscription_id, base_url=cloud_env.endpoints.resource_manager, ) client.config.add_user_agent("Salt/{0}".format(salt.version.__version__)) return client def log_cloud_error(client, message, **kwargs): """ Log an azurearm cloud error exception """ try: cloud_logger = getattr(log, kwargs.get("azurearm_log_level")) except (AttributeError, TypeError): cloud_logger = getattr(log, "error") cloud_logger( "An AzureARM %s CloudError has occurred: %s", client.capitalize(), message ) return def paged_object_to_list(paged_object): """ Extract all pages within a paged object as a list of dictionaries """ paged_return = [] while True: try: page = next(paged_object) paged_return.append(page.as_dict()) except CloudError: raise except StopIteration: break return paged_return def create_object_model(module_name, object_name, **kwargs): """ Assemble an object from incoming parameters. """ object_kwargs = {} try: model_module = importlib.import_module( "azure.mgmt.{0}.models".format(module_name) ) # pylint: disable=invalid-name Model = getattr(model_module, object_name) except ImportError: raise sys.exit( "The {0} model in the {1} Azure module is not available.".format( object_name, module_name ) ) if "_attribute_map" in dir(Model): for attr, items in Model._attribute_map.items(): param = kwargs.get(attr) if param is not None: if items["type"][0].isupper() and isinstance(param, dict): object_kwargs[attr] = create_object_model( module_name, items["type"], **param ) elif items["type"][0] == "{" and isinstance(param, dict): object_kwargs[attr] = param elif items["type"][0] == "[" and isinstance(param, list): obj_list = [] for list_item in param: if items["type"][1].isupper() and isinstance(list_item, dict): obj_list.append( create_object_model( module_name, items["type"][ items["type"].index("[") + 1 : items["type"].rindex("]") ], **list_item ) ) elif items["type"][1] == "{" and isinstance(list_item, dict): obj_list.append(list_item) elif not items["type"][1].isupper() and items["type"][1] != "{": obj_list.append(list_item) object_kwargs[attr] = obj_list else: object_kwargs[attr] = param # wrap calls to this function to catch TypeError exceptions return Model(**object_kwargs) def compare_list_of_dicts(old, new, convert_id_to_name=None): """ Compare lists of dictionaries representing Azure objects. Only keys found in the "new" dictionaries are compared to the "old" dictionaries, since getting Azure objects from the API returns some read-only data which should not be used in the comparison. A list of parameter names can be passed in order to compare a bare object name to a full Azure ID path for brevity. If string types are found in values, comparison is case insensitive. Return comment should be used to trigger exit from the calling function. """ ret = {} if not convert_id_to_name: convert_id_to_name = [] if not isinstance(new, list): ret["comment"] = "must be provided as a list of dictionaries!" return ret if len(new) != len(old): ret["changes"] = {"old": old, "new": new} return ret try: local_configs, remote_configs = [ sorted(config, key=itemgetter("name")) for config in (new, old) ] except TypeError: ret["comment"] = "configurations must be provided as a list of dictionaries!" return ret except KeyError: ret["comment"] = 'configuration dictionaries must contain the "name" key!' return ret for idx in six_range(0, len(local_configs)): for key in local_configs[idx]: local_val = local_configs[idx][key] if key in convert_id_to_name: remote_val = ( remote_configs[idx].get(key, {}).get("id", "").split("/")[-1] ) else: remote_val = remote_configs[idx].get(key) if isinstance(local_val, six.string_types): local_val = local_val.lower() if isinstance(remote_val, six.string_types): remote_val = remote_val.lower() if local_val != remote_val: ret["changes"] = {"old": remote_configs, "new": local_configs} return ret return ret