def run_state_tests(state, saltenv=None, check_all=False): ''' Execute tests for a salt state and return results Nested states will also be tested :param str state: state name for which to run associated .tst test files :param str saltenv: optional saltenv. Defaults to base :param bool check_all: boolean to run all tests in state/saltcheck-tests directory CLI Example: .. code-block:: bash salt '*' saltcheck.run_state_tests postfix,common ''' if not saltenv: if 'saltenv' in __opts__ and __opts__['saltenv']: saltenv = __opts__['saltenv'] else: saltenv = 'base' scheck = SaltCheck(saltenv) stl = StateTestLoader(saltenv) results = OrderedDict() sls_list = salt.utils.args.split_input(state) for state_name in sls_list: stl.add_test_files_for_sls(state_name, check_all) stl.load_test_suite() results_dict = OrderedDict() for key, value in stl.test_dict.items(): result = scheck.run_test(value) results_dict[key] = result if not results.get(state_name): # If passed a duplicate state, don't overwrite with empty res results[state_name] = results_dict return _generate_out_list(results)
def iostat(zpool=None, sample_time=0): ''' .. versionchanged:: 2016.3.0 Display I/O statistics for the given pools zpool : string optional name of storage pool sample_time : int seconds to capture data before output CLI Example: .. code-block:: bash salt '*' zpool.iostat myzpool ''' ret = OrderedDict() # get zpool list data zpool_cmd = _check_zpool() cmd = '{zpool_cmd} iostat -v{zpool}{sample_time}'.format( zpool_cmd=zpool_cmd, zpool=' {0}'.format(zpool) if zpool else '', sample_time=' {0} 2'.format(sample_time) if sample_time else '') res = __salt__['cmd.run_all'](cmd, python_shell=False) if res['retcode'] != 0: ret['error'] = res['stderr'] if 'stderr' in res else res['stdout'] return ret # note: hardcoded header fields, the double header is hard to parse # capacity operations bandwidth #pool alloc free read write read write header = [ 'pool', 'capacity-alloc', 'capacity-free', 'operations-read', 'operations-write', 'bandwith-read', 'bandwith-write' ] root_vdev = None vdev = None dev = None config_data = None current_pool = None for line in res['stdout'].splitlines(): if line.strip() == '': continue # ignore header if line.startswith('pool') and line.endswith('write'): continue if line.endswith('bandwidth'): continue if line.startswith('-') and line.endswith('-'): if config_data: ret[current_pool] = config_data config_data = OrderedDict() current_pool = None else: if not isinstance(config_data, salt.utils.odict.OrderedDict): continue stat_data = OrderedDict() stats = [x for x in line.strip().split(' ') if x not in ['']] for prop in header: if header.index(prop) < len(stats): if prop == 'pool': if not current_pool: current_pool = stats[header.index(prop)] continue if stats[header.index(prop)] == '-': continue stat_data[prop] = stats[header.index(prop)] dev = line.strip().split()[0] if line[0:4] != ' ': if line[0:2] == ' ': vdev = line.strip().split()[0] dev = None else: root_vdev = line.strip().split()[0] vdev = None dev = None if root_vdev: if not config_data.get(root_vdev): config_data[root_vdev] = {} if len(stat_data) > 0: config_data[root_vdev] = stat_data if vdev: if vdev not in config_data[root_vdev]: config_data[root_vdev][vdev] = {} if len(stat_data) > 0: config_data[root_vdev][vdev] = stat_data if dev and dev not in config_data[root_vdev][vdev]: config_data[root_vdev][vdev][dev] = {} if len(stat_data) > 0: config_data[root_vdev][vdev][dev] = stat_data return ret
def iostat(zpool=None, sample_time=0): ''' .. versionchanged:: 2016.3.0 Display I/O statistics for the given pools zpool : string optional name of storage pool sample_time : int seconds to capture data before output CLI Example: .. code-block:: bash salt '*' zpool.iostat myzpool ''' ret = OrderedDict() # get zpool list data zpool_cmd = _check_zpool() cmd = '{zpool_cmd} iostat -v{zpool}{sample_time}'.format( zpool_cmd=zpool_cmd, zpool=' {0}'.format(zpool) if zpool else '', sample_time=' {0} 2'.format(sample_time) if sample_time else '' ) res = __salt__['cmd.run_all'](cmd, python_shell=False) if res['retcode'] != 0: ret['error'] = res['stderr'] if 'stderr' in res else res['stdout'] return ret # note: hardcoded header fields, the double header is hard to parse # capacity operations bandwidth #pool alloc free read write read write header = [ 'pool', 'capacity-alloc', 'capacity-free', 'operations-read', 'operations-write', 'bandwith-read', 'bandwith-write' ] root_vdev = None vdev = None dev = None config_data = None current_pool = None for line in res['stdout'].splitlines(): if line.strip() == '': continue # ignore header if line.startswith('pool') and line.endswith('write'): continue if line.endswith('bandwidth'): continue if line.startswith('-') and line.endswith('-'): if config_data: ret[current_pool] = config_data config_data = OrderedDict() current_pool = None else: if not isinstance(config_data, salt.utils.odict.OrderedDict): continue stat_data = OrderedDict() stats = [x for x in line.strip().split(' ') if x not in ['']] for prop in header: if header.index(prop) < len(stats): if prop == 'pool': if not current_pool: current_pool = stats[header.index(prop)] continue if stats[header.index(prop)] == '-': continue stat_data[prop] = stats[header.index(prop)] dev = line.strip().split()[0] if line[0:4] != ' ': if line[0:2] == ' ': vdev = line.strip().split()[0] dev = None else: root_vdev = line.strip().split()[0] vdev = None dev = None if root_vdev: if not config_data.get(root_vdev): config_data[root_vdev] = {} if len(stat_data) > 0: config_data[root_vdev] = stat_data if vdev: if vdev not in config_data[root_vdev]: config_data[root_vdev][vdev] = {} if len(stat_data) > 0: config_data[root_vdev][vdev] = stat_data if dev and dev not in config_data[root_vdev][vdev]: config_data[root_vdev][vdev][dev] = {} if len(stat_data) > 0: config_data[root_vdev][vdev][dev] = stat_data return ret
def managed(name, entries, connect_spec=None): """Ensure the existence (or not) of LDAP entries and their attributes Example: .. code-block:: yaml ldapi:///: ldap.managed: - connect_spec: bind: method: sasl - entries: # make sure the entry doesn't exist - cn=foo,ou=users,dc=example,dc=com: - delete_others: True # make sure the entry exists with only the specified # attribute values - cn=admin,dc=example,dc=com: - delete_others: True - replace: cn: - admin description: - LDAP administrator objectClass: - simpleSecurityObject - organizationalRole userPassword: - {{pillar.ldap_admin_password}} # make sure the entry exists, its olcRootDN attribute # has only the specified value, the olcRootDN attribute # doesn't exist, and all other attributes are ignored - 'olcDatabase={1}hdb,cn=config': - replace: olcRootDN: - cn=admin,dc=example,dc=com # the admin entry has its own password attribute olcRootPW: [] # note the use of 'default'. also note how you don't # have to use list syntax if there is only one attribute # value - cn=foo,ou=users,dc=example,dc=com: - delete_others: True - default: userPassword: changeme shadowLastChange: 0 # keep sshPublicKey if present, but don't create # the attribute if it is missing sshPublicKey: [] - replace: cn: foo uid: foo uidNumber: 1000 gidNumber: 1000 gecos: Foo Bar givenName: Foo sn: Bar homeDirectory: /home/foo loginShell: /bin/bash objectClass: - inetOrgPerson - posixAccount - top - ldapPublicKey - shadowAccount :param name: The URL of the LDAP server. This is ignored if ``connect_spec`` is either a connection object or a dict with a ``'url'`` entry. :param entries: A description of the desired state of zero or more LDAP entries. ``entries`` is an iterable of dicts. Each of these dict's keys are the distinguished names (DNs) of LDAP entries to manage. Each of these dicts is processed in order. A later dict can reference an LDAP entry that was already mentioned in an earlier dict, which makes it possible for later dicts to enhance or alter the desired state of an LDAP entry. The DNs are mapped to a description of the LDAP entry's desired state. These LDAP entry descriptions are themselves iterables of dicts. Each dict in the iterable is processed in order. They contain directives controlling the entry's state. The key names the directive type and the value is state information for the directive. The specific structure of the state information depends on the directive type. The structure of ``entries`` looks like this:: [{dn1: [{directive1: directive1_state, directive2: directive2_state}, {directive3: directive3_state}], dn2: [{directive4: directive4_state, directive5: directive5_state}]}, {dn3: [{directive6: directive6_state}]}] These are the directives: * ``'delete_others'`` Boolean indicating whether to delete attributes not mentioned in this dict or any of the other directive dicts for this DN. Defaults to ``False``. If you don't want to delete an attribute if present, but you also don't want to add it if it is missing or modify it if it is present, you can use either the ``'default'`` directive or the ``'add'`` directive with an empty value list. * ``'default'`` A dict mapping an attribute name to an iterable of default values for that attribute. If the attribute already exists, it is left alone. If not, it is created using the given list of values. An empty value list is useful when you don't want to create an attribute if it is missing but you do want to preserve it if the ``'delete_others'`` key is ``True``. * ``'add'`` Attribute values to add to the entry. This is a dict mapping an attribute name to an iterable of values to add. An empty value list is useful when you don't want to create an attribute if it is missing but you do want to preserve it if the ``'delete_others'`` key is ``True``. * ``'delete'`` Attribute values to remove from the entry. This is a dict mapping an attribute name to an iterable of values to delete from the attribute. If the iterable is empty, all of the attribute's values are deleted. * ``'replace'`` Attributes to replace. This is a dict mapping an attribute name to an iterable of values. Any existing values for the attribute are deleted, then the given values are added. The iterable may be empty. In the above directives, the iterables of attribute values may instead be ``None``, in which case an empty list is used, or a scalar such as a string or number, in which case a new list containing the scalar is used. Note that if all attribute values are removed from an entry, the entire entry is deleted. :param connect_spec: See the description of the ``connect_spec`` parameter of the :py:func:`ldap3.connect <salt.modules.ldap3.connect>` function in the :py:mod:`ldap3 <salt.modules.ldap3>` execution module. If this is a dict and the ``'url'`` entry is not specified, the ``'url'`` entry is set to the value of the ``name`` parameter. :returns: A dict with the following keys: * ``'name'`` This is the same object passed to the ``name`` parameter. * ``'changes'`` This is a dict describing the changes made (or, in test mode, the changes that would have been attempted). If no changes were made (or no changes would have been attempted), then this dict is empty. Only successful changes are included. Each key is a DN of an entry that was changed (or would have been changed). Entries that were not changed (or would not have been changed) are not included. The value is a dict with two keys: * ``'old'`` The state of the entry before modification. If the entry did not previously exist, this key maps to ``None``. Otherwise, the value is a dict mapping each of the old entry's attributes to a list of its values before any modifications were made. Unchanged attributes are excluded from this dict. * ``'new'`` The state of the entry after modification. If the entry was deleted, this key maps to ``None``. Otherwise, the value is a dict mapping each of the entry's attributes to a list of its values after the modifications were made. Unchanged attributes are excluded from this dict. Example ``'changes'`` dict where a new entry was created with a single attribute containing two values:: {'dn1': {'old': None, 'new': {'attr1': ['val1', 'val2']}}} Example ``'changes'`` dict where a new attribute was added to an existing entry:: {'dn1': {'old': {}, 'new': {'attr2': ['val3']}}} * ``'result'`` One of the following values: * ``True`` if no changes were necessary or if all changes were applied successfully. * ``False`` if at least one change was unable to be applied. * ``None`` if changes would be applied but it is in test mode. """ if connect_spec is None: connect_spec = {} try: connect_spec.setdefault("url", name) except AttributeError: # already a connection object pass connect = __salt__["ldap3.connect"] # hack to get at the ldap3 module to access the ldap3.LDAPError # exception class. https://github.com/saltstack/salt/issues/27578 ldap3 = inspect.getmodule(connect) with connect(connect_spec) as l: old, new = _process_entries(l, entries) # collect all of the affected entries (only the key is # important in this dict; would have used an OrderedSet if # there was one) dn_set = OrderedDict() dn_set.update(old) dn_set.update(new) # do some cleanup dn_to_delete = set() for dn in dn_set: o = old.get(dn, {}) n = new.get(dn, {}) for x in o, n: to_delete = set() for attr, vals in x.items(): if not vals: # clean out empty attribute lists to_delete.add(attr) for attr in to_delete: del x[attr] if o == n: # clean out unchanged entries dn_to_delete.add(dn) for dn in dn_to_delete: for x in old, new: x.pop(dn, None) del dn_set[dn] ret = { "name": name, "changes": {}, "result": None, "comment": "", } if old == new: ret["comment"] = "LDAP entries already set" ret["result"] = True return ret if __opts__["test"]: ret["comment"] = "Would change LDAP entries" changed_old = old changed_new = new success_dn_set = dn_set else: # execute the changes changed_old = OrderedDict() changed_new = OrderedDict() # assume success; these will be changed on error ret["result"] = True ret["comment"] = "Successfully updated LDAP entries" errs = [] success_dn_set = OrderedDict() for dn in dn_set: o = old.get(dn, {}) n = new.get(dn, {}) try: # perform the operation if o: if n: op = "modify" assert o != n __salt__["ldap3.change"](l, dn, o, n) else: op = "delete" __salt__["ldap3.delete"](l, dn) else: op = "add" assert n __salt__["ldap3.add"](l, dn, n) # update these after the op in case an exception # is raised changed_old[dn] = o changed_new[dn] = n success_dn_set[dn] = True except ldap3.LDAPError as err: log.exception("failed to %s entry %s (%s)", op, dn, err) errs.append((op, dn, err)) continue if errs: ret["result"] = False ret["comment"] = "failed to " + ", ".join( (op + " entry " + dn + "(" + str(err) + ")" for op, dn, err in errs)) # set ret['changes']. filter out any unchanged attributes, and # convert the value sets to lists before returning them to the # user (sorted for easier comparisons) for dn in success_dn_set: o = changed_old.get(dn, {}) n = changed_new.get(dn, {}) changes = {} ret["changes"][dn] = changes for x, xn in ((o, "old"), (n, "new")): if not x: changes[xn] = None continue changes[xn] = { attr: sorted(vals) for attr, vals in x.items() if o.get(attr, ()) != n.get(attr, ()) } return ret
def _process_entries(l, entries): """Helper for managed() to process entries and return before/after views Collect the current database state and update it according to the data in :py:func:`managed`'s ``entries`` parameter. Return the current database state and what it will look like after modification. :param l: the LDAP connection object :param entries: the same object passed to the ``entries`` parameter of :py:func:`manage` :return: an ``(old, new)`` tuple that describes the current state of the entries and what they will look like after modification. Each item in the tuple is an OrderedDict that maps an entry DN to another dict that maps an attribute name to a set of its values (it's a set because according to the LDAP spec, attribute value ordering is unspecified and there can't be duplicates). The structure looks like this: {dn1: {attr1: set([val1])}, dn2: {attr1: set([val2]), attr2: set([val3, val4])}} All of an entry's attributes and values will be included, even if they will not be modified. If an entry mentioned in the entries variable doesn't yet exist in the database, the DN in ``old`` will be mapped to an empty dict. If an entry in the database will be deleted, the DN in ``new`` will be mapped to an empty dict. All value sets are non-empty: An attribute that will be added to an entry is not included in ``old``, and an attribute that will be deleted frm an entry is not included in ``new``. These are OrderedDicts to ensure that the user-supplied entries are processed in the user-specified order (in case there are dependencies, such as ACL rules specified in an early entry that make it possible to modify a later entry). """ old = OrderedDict() new = OrderedDict() for entries_dict in entries: for dn, directives_seq in entries_dict.items(): # get the old entry's state. first check to see if we've # previously processed the entry. olde = new.get(dn, None) if olde is None: # next check the database results = __salt__["ldap3.search"](l, dn, "base") if len(results) == 1: attrs = results[dn] olde = { attr: OrderedSet(attrs[attr]) for attr in attrs if len(attrs[attr]) } else: # nothing, so it must be a brand new entry assert len(results) == 0 olde = {} old[dn] = olde # copy the old entry to create the new (don't do a simple # assignment or else modifications to newe will affect # olde) newe = copy.deepcopy(olde) new[dn] = newe # process the directives entry_status = { "delete_others": False, "mentioned_attributes": set(), } for directives in directives_seq: _update_entry(newe, entry_status, directives) if entry_status["delete_others"]: to_delete = set() for attr in newe: if attr not in entry_status["mentioned_attributes"]: to_delete.add(attr) for attr in to_delete: del newe[attr] return old, new
def _process_entries(l, entries): '''Helper for managed() to process entries and return before/after views Collect the current database state and update it according to the data in :py:func:`managed`'s ``entries`` parameter. Return the current database state and what it will look like after modification. :param l: the LDAP connection object :param entries: the same object passed to the ``entries`` parameter of :py:func:`manage` :return: an ``(old, new)`` tuple that describes the current state of the entries and what they will look like after modification. Each item in the tuple is an OrderedDict that maps an entry DN to another dict that maps an attribute name to a set of its values (it's a set because according to the LDAP spec, attribute value ordering is unspecified and there can't be duplicates). The structure looks like this: {dn1: {attr1: set([val1])}, dn2: {attr1: set([val2]), attr2: set([val3, val4])}} All of an entry's attributes and values will be included, even if they will not be modified. If an entry mentioned in the entries variable doesn't yet exist in the database, the DN in ``old`` will be mapped to an empty dict. If an entry in the database will be deleted, the DN in ``new`` will be mapped to an empty dict. All value sets are non-empty: An attribute that will be added to an entry is not included in ``old``, and an attribute that will be deleted frm an entry is not included in ``new``. These are OrderedDicts to ensure that the user-supplied entries are processed in the user-specified order (in case there are dependencies, such as ACL rules specified in an early entry that make it possible to modify a later entry). ''' old = OrderedDict() new = OrderedDict() for entries_dict in entries: for dn, directives_seq in six.iteritems(entries_dict): # get the old entry's state. first check to see if we've # previously processed the entry. olde = new.get(dn, None) if olde is None: # next check the database results = __salt__['ldap3.search'](l, dn, 'base') if len(results) == 1: attrs = results[dn] olde = dict(((attr, set(attrs[attr])) for attr in attrs if len(attrs[attr]))) else: # nothing, so it must be a brand new entry assert len(results) == 0 olde = {} old[dn] = olde # copy the old entry to create the new (don't do a simple # assignment or else modifications to newe will affect # olde) newe = copy.deepcopy(olde) new[dn] = newe # process the directives entry_status = { 'delete_others': False, 'mentioned_attributes': set(), } for directives in directives_seq: _update_entry(newe, entry_status, directives) if entry_status['delete_others']: to_delete = set() for attr in newe: if attr not in entry_status['mentioned_attributes']: to_delete.add(attr) for attr in to_delete: del newe[attr] return old, new
def managed(name, entries, connect_spec=None): '''Ensure the existance (or not) of LDAP entries and their attributes Example: .. code-block:: yaml ldapi:///: ldap.managed: - connect_spec: bind: method: sasl - entries: # make sure the entry doesn't exist - cn=foo,ou=users,dc=example,dc=com: - delete_others: True # make sure the entry exists with only the specified # attribute values - cn=admin,dc=example,dc=com: - delete_others: True - replace: cn: - admin description: - LDAP administrator objectClass: - simpleSecurityObject - organizationalRole userPassword: - {{pillar.ldap_admin_password}} # make sure the entry exists, its olcRootDN attribute # has only the specified value, the olcRootDN attribute # doesn't exist, and all other attributes are ignored - 'olcDatabase={1}hdb,cn=config': - replace: olcRootDN: - cn=admin,dc=example,dc=com # the admin entry has its own password attribute olcRootPW: [] # note the use of 'default'. also note how you don't # have to use list syntax if there is only one attribute # value - cn=foo,ou=users,dc=example,dc=com: - delete_others: True - default: userPassword: changeme shadowLastChange: 0 # keep sshPublicKey if present, but don't create # the attribute if it is missing sshPublicKey: [] - replace: cn: foo uid: foo uidNumber: 1000 gidNumber: 1000 gecos: Foo Bar givenName: Foo sn: Bar homeDirectory: /home/foo loginShell: /bin/bash objectClass: - inetOrgPerson - posixAccount - top - ldapPublicKey - shadowAccount :param name: The URL of the LDAP server. This is ignored if ``connect_spec`` is either a connection object or a dict with a ``'url'`` entry. :param entries: A description of the desired state of zero or more LDAP entries. ``entries`` is an iterable of dicts. Each of these dict's keys are the distinguished names (DNs) of LDAP entries to manage. Each of these dicts is processed in order. A later dict can reference an LDAP entry that was already mentioned in an earlier dict, which makes it possible for later dicts to enhance or alter the desired state of an LDAP entry. The DNs are mapped to a description of the LDAP entry's desired state. These LDAP entry descriptions are themselves iterables of dicts. Each dict in the iterable is processed in order. They contain directives controlling the entry's state. The key names the directive type and the value is state information for the directive. The specific structure of the state information depends on the directive type. The structure of ``entries`` looks like this:: [{dn1: [{directive1: directive1_state, directive2: directive2_state}, {directive3: directive3_state}], dn2: [{directive4: directive4_state, directive5: directive5_state}]}, {dn3: [{directive6: directive6_state}]}] These are the directives: * ``'delete_others'`` Boolean indicating whether to delete attributes not mentioned in this dict or any of the other directive dicts for this DN. Defaults to ``False``. If you don't want to delete an attribute if present, but you also don't want to add it if it is missing or modify it if it is present, you can use either the ``'default'`` directive or the ``'add'`` directive with an empty value list. * ``'default'`` A dict mapping an attribute name to an iterable of default values for that attribute. If the attribute already exists, it is left alone. If not, it is created using the given list of values. An empty value list is useful when you don't want to create an attribute if it is missing but you do want to preserve it if the ``'delete_others'`` key is ``True``. * ``'add'`` Attribute values to add to the entry. This is a dict mapping an attribute name to an iterable of values to add. An empty value list is useful when you don't want to create an attribute if it is missing but you do want to preserve it if the ``'delete_others'`` key is ``True``. * ``'delete'`` Attribute values to remove from the entry. This is a dict mapping an attribute name to an iterable of values to delete from the attribute. If the iterable is empty, all of the attribute's values are deleted. * ``'replace'`` Attributes to replace. This is a dict mapping an attribute name to an iterable of values. Any existing values for the attribute are deleted, then the given values are added. The iterable may be empty. In the above directives, the iterables of attribute values may instead be ``None``, in which case an empty list is used, or a scalar such as a string or number, in which case a new list containing the scalar is used. Note that if all attribute values are removed from an entry, the entire entry is deleted. :param connect_spec: See the description of the ``connect_spec`` parameter of the :py:func:`ldap3.connect <salt.modules.ldap3.connect>` function in the :py:mod:`ldap3 <salt.modules.ldap3>` execution module. If this is a dict and the ``'url'`` entry is not specified, the ``'url'`` entry is set to the value of the ``name`` parameter. :returns: A dict with the following keys: * ``'name'`` This is the same object passed to the ``name`` parameter. * ``'changes'`` This is a dict describing the changes made (or, in test mode, the changes that would have been attempted). If no changes were made (or no changes would have been attempted), then this dict is empty. Only successful changes are included. Each key is a DN of an entry that was changed (or would have been changed). Entries that were not changed (or would not have been changed) are not included. The value is a dict with two keys: * ``'old'`` The state of the entry before modification. If the entry did not previously exist, this key maps to ``None``. Otherwise, the value is a dict mapping each of the old entry's attributes to a list of its values before any modifications were made. Unchanged attributes are excluded from this dict. * ``'new'`` The state of the entry after modification. If the entry was deleted, this key maps to ``None``. Otherwise, the value is a dict mapping each of the entry's attributes to a list of its values after the modifications were made. Unchanged attributes are excluded from this dict. Example ``'changes'`` dict where a new entry was created with a single attribute containing two values:: {'dn1': {'old': None, 'new': {'attr1': ['val1', 'val2']}}} Example ``'changes'`` dict where a new attribute was added to an existing entry:: {'dn1': {'old': {}, 'new': {'attr2': ['val3']}}} * ``'result'`` One of the following values: * ``True`` if no changes were necessary or if all changes were applied successfully. * ``False`` if at least one change was unable to be applied. * ``None`` if changes would be applied but it is in test mode. ''' if connect_spec is None: connect_spec = {} try: connect_spec.setdefault('url', name) except AttributeError: # already a connection object pass connect = __salt__['ldap3.connect'] # hack to get at the ldap3 module to access the ldap3.LDAPError # exception class. https://github.com/saltstack/salt/issues/27578 ldap3 = inspect.getmodule(connect) with connect(connect_spec) as l: old, new = _process_entries(l, entries) # collect all of the affected entries (only the key is # important in this dict; would have used an OrderedSet if # there was one) dn_set = OrderedDict() dn_set.update(old) dn_set.update(new) # do some cleanup dn_to_delete = set() for dn in dn_set: o = old.get(dn, {}) n = new.get(dn, {}) for x in o, n: to_delete = set() for attr, vals in six.iteritems(x): if not len(vals): # clean out empty attribute lists to_delete.add(attr) for attr in to_delete: del x[attr] if o == n: # clean out unchanged entries dn_to_delete.add(dn) for dn in dn_to_delete: for x in old, new: x.pop(dn, None) del dn_set[dn] ret = { 'name': name, 'changes': {}, 'result': None, 'comment': '', } if old == new: ret['comment'] = 'LDAP entries already set' ret['result'] = True return ret if __opts__['test']: ret['comment'] = 'Would change LDAP entries' changed_old = old changed_new = new success_dn_set = dn_set else: # execute the changes changed_old = OrderedDict() changed_new = OrderedDict() # assume success; these will be changed on error ret['result'] = True ret['comment'] = 'Successfully updated LDAP entries' errs = [] success_dn_set = OrderedDict() for dn in dn_set: o = old.get(dn, {}) n = new.get(dn, {}) try: # perform the operation if len(o): if len(n): op = 'modify' assert o != n __salt__['ldap3.change'](l, dn, o, n) else: op = 'delete' __salt__['ldap3.delete'](l, dn) else: op = 'add' assert len(n) __salt__['ldap3.add'](l, dn, n) # update these after the op in case an exception # is raised changed_old[dn] = o changed_new[dn] = n success_dn_set[dn] = True except ldap3.LDAPError: log.exception('failed to %s entry %s', op, dn) errs.append((op, dn)) continue if len(errs): ret['result'] = False ret['comment'] = 'failed to ' \ + ', '.join((op + ' entry ' + dn for op, dn in errs)) # set ret['changes']. filter out any unchanged attributes, and # convert the value sets to lists before returning them to the # user (sorted for easier comparisons) for dn in success_dn_set: o = changed_old.get(dn, {}) n = changed_new.get(dn, {}) changes = {} ret['changes'][dn] = changes for x, xn in ((o, 'old'), (n, 'new')): if not len(x): changes[xn] = None continue changes[xn] = dict(((attr, sorted(vals)) for attr, vals in six.iteritems(x) if o.get(attr, ()) != n.get(attr, ()))) return ret
def run_state_tests(state, saltenv=None, check_all=False, only_fails=False): """ Execute tests for a salt state and return results Nested states will also be tested :param str state: state name for which to run associated .tst test files :param str saltenv: optional saltenv. Defaults to base :param bool check_all: boolean to run all tests in state/saltcheck-tests directory :param bool only_fails: boolean to only print failure results CLI Example: .. code-block:: bash salt '*' saltcheck.run_state_tests postfix,common Tests will be run in parallel by adding "saltcheck_parallel: True" in minion config. When enabled, saltcheck will use up to the number of cores detected. This can be limited by setting the "saltcheck_processes" value to an integer to set the maximum number of parallel processes. """ if not saltenv: if "saltenv" in __opts__ and __opts__["saltenv"]: saltenv = __opts__["saltenv"] else: saltenv = "base" # Use global scheck variable for reuse in each multiprocess global global_scheck global_scheck = SaltCheck(saltenv) parallel = __salt__["config.get"]("saltcheck_parallel") num_proc = __salt__["config.get"]("saltcheck_processes") stl = StateTestLoader(saltenv) results = OrderedDict() sls_list = salt.utils.args.split_input(state) for state_name in sls_list: stl.add_test_files_for_sls(state_name, check_all) stl.load_test_suite() results_dict = OrderedDict() # Check for situations to disable parallization if parallel: if type(num_proc) == float: num_proc = int(num_proc) if multiprocessing.cpu_count() < 2: parallel = False log.debug("Only 1 CPU. Disabling parallization.") elif num_proc == 1: # Don't bother with multiprocessing overhead parallel = False log.debug( "Configuration limited to 1 CPU. Disabling parallization.") else: for items in stl.test_dict.values(): if "state.apply" in items.get("module_and_function", []): # Multiprocessing doesn't ensure ordering, which state.apply # might require parallel = False log.warning( "Tests include state.apply. Disabling parallization." ) if parallel: if num_proc: pool_size = num_proc else: pool_size = min(len(stl.test_dict), multiprocessing.cpu_count()) log.debug("Running tests in parallel with %s processes", pool_size) presults = multiprocessing.Pool(pool_size).map( func=parallel_scheck, iterable=stl.test_dict.items()) # Remove list and form expected data structure for item in presults: for key, value in item.items(): results_dict[key] = value else: for key, value in stl.test_dict.items(): result = global_scheck.run_test(value) results_dict[key] = result # If passed a duplicate state, don't overwrite with empty res if not results.get(state_name): results[state_name] = results_dict return _generate_out_list(results, only_fails=only_fails)
class PathInfoDict(fileinfo.FileInfo): def __init__(self, match_each=True, **patterns): ''' match_each: If True, each file path is matched which prevents uses less memory but sacrifices performance a little bit. If False, the complete list is matched after all the file information has been added to pathinfo. patterns: Contains the patterns to match. Example: { 'saltenv': 'base', 'relpath': ['*.sls'] } ''' super(PathInfoDict, self).__init__( fields=PATHINFO_FIELDS, match_each=match_each, **patterns ) self._elements = OrderedDict() @property def as_sequence(self): if self.pattern and not self.match_each: return list( matcher.ifilter( self._elements.values(), _pattern=self.pattern ) ) return self._elements.values() def element(self, root=None, abspath=None, **kwargs): ''' kwargs contain extra information for custom methods. This method must return a valid empty object if no vars are passed to allow introspection to create patterns. :param root: :param abspath: ''' if root is None and abspath is None: root = os.path.abspath('.') abspath = os.path.abspath('.') relpath = os.path.relpath(abspath, root) try: element = self._elements.get(relpath, OrderedDict()) except AttributeError: element = OrderedDict() if not element: for field in PATHINFO_FIELDS: element.setdefault(field, '') element['saltenv'] = kwargs.get('saltenv', 'base') element['relpath'] = relpath element['abspath'] = abspath element['is_pillar'] = kwargs.get('is_pillar', False) cachedir = kwargs.get('cachedir', '') if cachedir and os.path.commonprefix([abspath, cachedir]) == cachedir: element['cache_root'] = root else: element['file_root'] = root element_hook = kwargs.get('_element_hook', None) if element_hook: element = element_hook(self, element, **kwargs) return element def add_element(self, element, **kwargs): add_hook = kwargs.get('_add_hook', None) if add_hook: element = add_hook(self, element, **kwargs) if element['relpath'] not in self._elements: self._elements[element['relpath']] = element def filelist(self, roots, **kwargs): ''' :param roots: file_roots, pillar_roots, cache_roots, etc to walk. kwargs: Contains any extra variables to pass to element. ''' for env, destdirs in six.iteritems(roots): kwargs['saltenv'] = env super(PathInfoDict, self).filelist(destdirs, **kwargs) return self.as_sequence
class PathInfoDict(fileinfo.FileInfo): def __init__(self, match_each=True, **patterns): ''' match_each: If True, each file path is matched which prevents uses less memory but sacrifices performance a little bit. If False, the complete list is matched after all the file information has been added to pathinfo. patterns: Contains the patterns to match. Example: { 'saltenv': 'base', 'relpath': ['*.sls'] } ''' super(PathInfoDict, self).__init__(fields=PATHINFO_FIELDS, match_each=match_each, **patterns) self._elements = OrderedDict() @property def as_sequence(self): if self.pattern and not self.match_each: return list( matcher.ifilter(self._elements.values(), _pattern=self.pattern)) return self._elements.values() def element(self, root=None, abspath=None, **kwargs): ''' kwargs contain extra information for custom methods. This method must return a valid empty object if no vars are passed to allow introspection to create patterns. :param root: :param abspath: ''' if root is None and abspath is None: root = os.path.abspath('.') abspath = os.path.abspath('.') relpath = os.path.relpath(abspath, root) try: element = self._elements.get(relpath, OrderedDict()) except AttributeError: element = OrderedDict() if not element: for field in PATHINFO_FIELDS: element.setdefault(field, '') element['saltenv'] = kwargs.get('saltenv', 'base') element['relpath'] = relpath element['abspath'] = abspath element['is_pillar'] = kwargs.get('is_pillar', False) cachedir = kwargs.get('cachedir', '') if cachedir and os.path.commonprefix([abspath, cachedir]) == cachedir: element['cache_root'] = root else: element['file_root'] = root element_hook = kwargs.get('_element_hook', None) if element_hook: element = element_hook(self, element, **kwargs) return element def add_element(self, element, **kwargs): add_hook = kwargs.get('_add_hook', None) if add_hook: element = add_hook(self, element, **kwargs) if element['relpath'] not in self._elements: self._elements[element['relpath']] = element def filelist(self, roots, **kwargs): ''' :param roots: file_roots, pillar_roots, cache_roots, etc to walk. kwargs: Contains any extra variables to pass to element. ''' for env, destdirs in six.iteritems(roots): kwargs['saltenv'] = env super(PathInfoDict, self).filelist(destdirs, **kwargs) return self.as_sequence