1"""
2A general map/reduce style salt runner for aggregating results
3returned by several different minions.
4
5.. versionadded:: 2014.7.0
6
7Aggregated results are sorted by the size of the minion pools which returned
8matching results.
9
10Useful for playing the game: *"some of these things are not like the others..."*
11when identifying discrepancies in a large infrastructure managed by salt.
12"""
13
14import salt.client
15from salt.exceptions import SaltClientError
16
17
18def hash(*args, **kwargs):
19    """
20    Return the MATCHING minion pools from the aggregated and sorted results of
21    a salt command
22
23    .. versionadded:: 2014.7.0
24
25    This command is submitted via a salt runner using the
26    general form::
27
28        salt-run survey.hash [survey_sort=up/down] <target>
29                  <salt-execution-module> <salt-execution-module parameters>
30
31    Optionally accept a ``survey_sort=`` parameter. Default: ``survey_sort=down``
32
33    CLI Example #1: (functionally equivalent to ``salt-run manage.up``)
34
35    .. code-block:: bash
36
37        salt-run survey.hash "*" test.ping
38
39    CLI Example #2: (find an "outlier" minion config file)
40
41    .. code-block:: bash
42
43        salt-run survey.hash "*" file.get_hash /etc/salt/minion survey_sort=up
44    """
45
46    return _get_pool_results(*args, **kwargs)
47
48
49def diff(*args, **kwargs):
50    """
51    Return the DIFFERENCE of the result sets returned by each matching minion
52    pool
53
54    .. versionadded:: 2014.7.0
55
56    These pools are determined from the aggregated and sorted results of
57    a salt command.
58
59    This command displays the "diffs" as a series of 2-way differences --
60    namely the difference between the FIRST displayed minion pool
61    (according to sort order) and EACH SUBSEQUENT minion pool result set.
62
63    Differences are displayed according to the Python ``difflib.unified_diff()``
64    as in the case of the salt execution module ``file.get_diff``.
65
66    This command is submitted via a salt runner using the general form::
67
68        salt-run survey.diff [survey_sort=up/down] <target>
69                     <salt-execution-module> <salt-execution-module parameters>
70
71    Optionally accept a ``survey_sort=`` parameter. Default:
72    ``survey_sort=down``
73
74    CLI Example #1: (Example to display the "differences of files")
75
76    .. code-block:: bash
77
78        salt-run survey.diff survey_sort=up "*" cp.get_file_str file:///etc/hosts
79    """
80    # TODO: The salt execution module "cp.get_file_str file:///..." is a
81    # non-obvious way to display the differences between files using
82    # survey.diff .  A more obvious method needs to be found or developed.
83
84    import difflib
85
86    bulk_ret = _get_pool_results(*args, **kwargs)
87
88    is_first_time = True
89    for k in bulk_ret:
90        print("minion pool :\n------------")
91        print(k["pool"])
92        print("pool size :\n----------")
93        print("    " + str(len(k["pool"])))
94        if is_first_time:
95            is_first_time = False
96            print("pool result :\n------------")
97            print("    " + bulk_ret[0]["result"])
98            print()
99            continue
100
101        outs = 'differences from "{}" results :'.format(bulk_ret[0]["pool"][0])
102        print(outs)
103        print("-" * (len(outs) - 1))
104        from_result = bulk_ret[0]["result"].splitlines()
105        for idx, _ in enumerate(from_result):
106            from_result[idx] += "\n"
107        to_result = k["result"].splitlines()
108        for idx, _ in enumerate(to_result):
109            to_result[idx] += "\n"
110        outs = ""
111        outs += "".join(
112            difflib.unified_diff(
113                from_result,
114                to_result,
115                fromfile=bulk_ret[0]["pool"][0],
116                tofile=k["pool"][0],
117                n=0,
118            )
119        )
120        print(outs)
121        print()
122
123    return bulk_ret
124
125
126def _get_pool_results(*args, **kwargs):
127    """
128    A helper function which returns a dictionary of minion pools along with
129    their matching result sets.
130    Useful for developing other "survey style" functions.
131    Optionally accepts a "survey_sort=up" or "survey_sort=down" kwargs for
132    specifying sort order.
133    Because the kwargs namespace of the "salt" and "survey" command are shared,
134    the name "survey_sort" was chosen to help avoid option conflicts.
135    """
136    # TODO: the option "survey.sort=" would be preferred for namespace
137    # separation but the kwargs parser for the salt-run command seems to
138    # improperly pass the options containing a "." in them for later modules to
139    # process. The "_" is used here instead.
140
141    import hashlib
142
143    tgt = args[0]
144    cmd = args[1]
145    ret = {}
146
147    sort = kwargs.pop("survey_sort", "down")
148    direction = sort != "up"
149
150    tgt_type = kwargs.pop("tgt_type", "compound")
151    if tgt_type not in ["compound", "pcre"]:
152        tgt_type = "compound"
153
154    kwargs_passthru = {
155        key: value for (key, value) in kwargs.items() if not key.startswith("_")
156    }
157
158    with salt.client.get_local_client(__opts__["conf_file"]) as client:
159        try:
160            minions = client.cmd(
161                tgt,
162                cmd,
163                args[2:],
164                timeout=__opts__["timeout"],
165                tgt_type=tgt_type,
166                kwarg=kwargs_passthru,
167            )
168        except SaltClientError as client_error:
169            print(client_error)
170            return ret
171
172    # hash minion return values as a string
173    for minion in sorted(minions):
174        digest = hashlib.sha256(
175            str(minions[minion]).encode(__salt_system_encoding__)
176        ).hexdigest()
177        if digest not in ret:
178            ret[digest] = {}
179            ret[digest]["pool"] = []
180            ret[digest]["result"] = str(minions[minion])
181
182        ret[digest]["pool"].append(minion)
183
184    sorted_ret = []
185    for k in sorted(ret, key=lambda k: len(ret[k]["pool"]), reverse=direction):
186        # return aggregated results, sorted by size of the hash pool
187
188        sorted_ret.append(ret[k])
189
190    return sorted_ret
191