1"""
2Vistara Runner
3
4Runner to interact with the Vistara (http://www.vistarait.com/) REST API
5
6:codeauthor: Brad Thurber <brad.thurber@gmail.com>
7
8To use this runner, the Vistara client_id and Vistara oauth2 client_key
9and client_secret must be set in the master config.
10
11For example ``/etc/salt/master.d/_vistara.conf``:
12
13.. code-block:: yaml
14
15    vistara:
16      client_id: client_012345
17      client_key: N0tReallyaR3alKeyButShouldB12345
18      client_secret: ThisI5AreallyLongsecretKeyIwonderwhyTheyMakethemSoBigTheseDays00
19
20
21"""
22
23import logging
24
25import salt.output
26
27# See https://docs.saltproject.io/en/latest/topics/tutorials/http.html
28import salt.utils.http
29
30log = logging.getLogger(__name__)
31
32
33def __virtual__():
34    """
35    Check to see if master config has the necessary config
36    """
37    vistara_config = __opts__["vistara"] if "vistara" in __opts__ else None
38
39    if vistara_config:
40        client_id = vistara_config.get("client_id", None)
41        client_key = vistara_config.get("client_key", None)
42        client_secret = vistara_config.get("client_secret", None)
43
44        if not client_id or not client_key or not client_secret:
45            return (
46                False,
47                "vistara client_id or client_key or client_secret "
48                "has not been specified in the Salt master config.",
49            )
50        return True
51
52    return (
53        False,
54        "vistara config has not been specificed in the Salt master "
55        "config. See documentation for this runner.",
56    )
57
58
59def _get_vistara_configuration():
60    """
61    Return the Vistara configuration read from the master config
62    """
63    return {
64        "client_id": __opts__["vistara"]["client_id"],
65        "client_key": __opts__["vistara"]["client_key"],
66        "client_secret": __opts__["vistara"]["client_secret"],
67    }
68
69
70def delete_device(name, safety_on=True):
71    """
72    Deletes a device from Vistara based on DNS name or partial name. By default,
73    delete_device will only perform the delete if a single host is returned. Set
74    safety_on=False to delete all matches (up to default API search page size)
75
76    CLI Example:
77
78    .. code-block:: bash
79
80        salt-run vistara.delete_device 'hostname-101.mycompany.com'
81        salt-run vistara.delete_device 'hostname-101'
82        salt-run vistara.delete_device 'hostname-1' safety_on=False
83
84    """
85
86    config = _get_vistara_configuration()
87    if not config:
88        return False
89
90    access_token = _get_oath2_access_token(
91        config["client_key"], config["client_secret"]
92    )
93
94    if not access_token:
95        return "Vistara access token not available"
96
97    query_string = "dnsName:{}".format(name)
98
99    devices = _search_devices(query_string, config["client_id"], access_token)
100
101    if not devices:
102        return "No devices found"
103
104    device_count = len(devices)
105
106    if safety_on and device_count != 1:
107        return (
108            "Expected to delete 1 device and found {}. "
109            "Set safety_on=False to override.".format(device_count)
110        )
111
112    delete_responses = []
113    for device in devices:
114        device_id = device["id"]
115        log.debug(device_id)
116        delete_response = _delete_resource(device_id, config["client_id"], access_token)
117        if not delete_response:
118            return False
119        delete_responses.append(delete_response)
120
121    return delete_responses
122
123
124def _search_devices(query_string, client_id, access_token):
125
126    authstring = "Bearer {}".format(access_token)
127
128    headers = {
129        "Authorization": authstring,
130        "Content-Type": "application/json",
131        "Accept": "application/json",
132    }
133
134    params = {"queryString": query_string}
135
136    method = "GET"
137    url = "https://api.vistara.io/api/v2/tenants/{}/devices/search".format(client_id)
138
139    resp = salt.utils.http.query(
140        url=url, method=method, header_dict=headers, params=params, opts=__opts__
141    )
142
143    respbody = resp.get("body", None)
144    if not respbody:
145        return False
146
147    respbodydict = salt.utils.json.loads(resp["body"])
148    deviceresults = respbodydict["results"]
149
150    return deviceresults
151
152
153def _delete_resource(device_id, client_id, access_token):
154
155    authstring = "Bearer {}".format(access_token)
156
157    headers = {
158        "Authorization": authstring,
159        "Content-Type": "application/json",
160        "Accept": "application/json",
161    }
162
163    method = "DELETE"
164    url = "https://api.vistara.io/api/v2/tenants/{}/rtype/DEVICE/resource/{}".format(
165        client_id, device_id
166    )
167
168    resp = salt.utils.http.query(
169        url=url, method=method, header_dict=headers, opts=__opts__
170    )
171
172    respbody = resp.get("body", None)
173    if not respbody:
174        return False
175
176    respbodydict = salt.utils.json.loads(resp["body"])
177
178    return respbodydict
179
180
181def _get_oath2_access_token(client_key, client_secret):
182    """
183    Query the vistara API and get an access_token
184
185    """
186    if not client_key and not client_secret:
187        log.error(
188            "client_key and client_secret have not been specified "
189            "and are required parameters."
190        )
191        return False
192
193    method = "POST"
194    url = "https://api.vistara.io/auth/oauth/token"
195    headers = {
196        "Content-Type": "application/x-www-form-urlencoded",
197        "Accept": "application/json",
198    }
199
200    params = {
201        "grant_type": "client_credentials",
202        "client_id": client_key,
203        "client_secret": client_secret,
204    }
205
206    resp = salt.utils.http.query(
207        url=url, method=method, header_dict=headers, params=params, opts=__opts__
208    )
209
210    respbody = resp.get("body", None)
211
212    if not respbody:
213        return False
214
215    access_token = salt.utils.json.loads(respbody)["access_token"]
216    return access_token
217