1"""
2Roster matching by various criteria (glob, pcre, etc)
3"""
4
5import copy
6import fnmatch
7import functools
8import logging
9import re
10
11# Try to import range from https://github.com/ytoolshed/range
12HAS_RANGE = False
13try:
14    import seco.range
15
16    HAS_RANGE = True
17except ImportError:
18    pass
19# pylint: enable=import-error
20
21
22log = logging.getLogger(__name__)
23
24
25def targets(conditioned_raw, tgt, tgt_type, ipv="ipv4"):
26    rmatcher = RosterMatcher(conditioned_raw, tgt, tgt_type, ipv)
27    return rmatcher.targets()
28
29
30def _tgt_set(tgt):
31    """
32    Return the tgt as a set of literal names
33    """
34    try:
35        # A comma-delimited string
36        return set(tgt.split(","))
37    except AttributeError:
38        # Assume tgt is already a non-string iterable.
39        return set(tgt)
40
41
42class RosterMatcher:
43    """
44    Matcher for the roster data structure
45    """
46
47    def __init__(self, raw, tgt, tgt_type, ipv="ipv4"):
48        self.tgt = tgt
49        self.tgt_type = tgt_type
50        self.raw = raw
51        self.ipv = ipv
52
53    def targets(self):
54        """
55        Execute the correct tgt_type routine and return
56        """
57        try:
58            return getattr(self, "ret_{}_minions".format(self.tgt_type))()
59        except AttributeError:
60            return {}
61
62    def _ret_minions(self, filter_):
63        """
64        Filter minions by a generic filter.
65        """
66        minions = {}
67        for minion in filter_(self.raw):
68            data = self.get_data(minion)
69            if data:
70                minions[minion] = data.copy()
71        return minions
72
73    def ret_glob_minions(self):
74        """
75        Return minions that match via glob
76        """
77        fnfilter = functools.partial(fnmatch.filter, pat=self.tgt)
78        return self._ret_minions(fnfilter)
79
80    def ret_pcre_minions(self):
81        """
82        Return minions that match via pcre
83        """
84        tgt = re.compile(self.tgt)
85        refilter = functools.partial(filter, tgt.match)
86        return self._ret_minions(refilter)
87
88    def ret_list_minions(self):
89        """
90        Return minions that match via list
91        """
92        tgt = _tgt_set(self.tgt)
93        return self._ret_minions(tgt.intersection)
94
95    def ret_nodegroup_minions(self):
96        """
97        Return minions which match the special list-only groups defined by
98        ssh_list_nodegroups
99        """
100        nodegroup = __opts__.get("ssh_list_nodegroups", {}).get(self.tgt, [])
101        nodegroup = _tgt_set(nodegroup)
102        return self._ret_minions(nodegroup.intersection)
103
104    def ret_range_minions(self):
105        """
106        Return minions that are returned by a range query
107        """
108        if HAS_RANGE is False:
109            raise RuntimeError("Python lib 'seco.range' is not available")
110
111        minions = {}
112        range_hosts = _convert_range_to_list(self.tgt, __opts__["range_server"])
113        return self._ret_minions(range_hosts.__contains__)
114
115    def get_data(self, minion):
116        """
117        Return the configured ip
118        """
119        ret = copy.deepcopy(__opts__.get("roster_defaults", {}))
120        if isinstance(self.raw[minion], str):
121            ret.update({"host": self.raw[minion]})
122            return ret
123        elif isinstance(self.raw[minion], dict):
124            ret.update(self.raw[minion])
125            return ret
126        return False
127
128
129def _convert_range_to_list(tgt, range_server):
130    """
131    convert a seco.range range into a list target
132    """
133    r = seco.range.Range(range_server)
134    try:
135        return r.expand(tgt)
136    except seco.range.RangeException as err:
137        log.error("Range server exception: %s", err)
138        return []
139