1# -*- coding: utf-8 -*-
2# Copyright: (c) 2018, Mikhail Yohman (@fragmentedpacket) <mikhail.yohman@gmail.com>
3# Copyright: (c) 2018, David Gomez (@amb1s1) <david.gomez@networktocode.com>
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6__metaclass__ = type
7
8API_APPS_ENDPOINTS = dict(
9    circuits=[],
10    dcim=[
11        "devices",
12        "device_roles",
13        "device_types",
14        "devices",
15        "interfaces",
16        "platforms",
17        "racks",
18        "regions",
19        "sites",
20    ],
21    extras=[],
22    ipam=["ip_addresses", "prefixes", "roles", "vlans", "vlan_groups", "vrfs"],
23    secrets=[],
24    tenancy=["tenants", "tenant_groups"],
25    virtualization=["clusters"],
26)
27
28QUERY_TYPES = dict(
29    cluster="name",
30    devices="name",
31    device_role="slug",
32    device_type="slug",
33    manufacturer="slug",
34    nat_inside="address",
35    nat_outside="address",
36    platform="slug",
37    primary_ip="address",
38    primary_ip4="address",
39    primary_ip6="address",
40    rack="slug",
41    region="slug",
42    role="slug",
43    site="slug",
44    tenant="name",
45    tenant_group="slug",
46    time_zone="timezone",
47    vlan="name",
48    vlan_group="slug",
49    vrf="name",
50)
51
52CONVERT_TO_ID = dict(
53    cluster="clusters",
54    device="devices",
55    device_role="device_roles",
56    device_type="device_types",
57    interface="interfaces",
58    lag="interfaces",
59    nat_inside="ip_addresses",
60    nat_outside="ip_addresses",
61    platform="platforms",
62    primary_ip="ip_addresses",
63    primary_ip4="ip_addresses",
64    primary_ip6="ip_addresses",
65    rack="racks",
66    region="regions",
67    role="roles",
68    site="sites",
69    tagged_vlans="vlans",
70    tenant="tenants",
71    tenant_group="tenant_groups",
72    untagged_vlan="vlans",
73    vlan="vlans",
74    vlan_group="vlan_groups",
75    vrf="vrfs",
76)
77
78FACE_ID = dict(front=0, rear=1)
79
80NO_DEFAULT_ID = set(
81    [
82        "device",
83        "lag",
84        "primary_ip",
85        "primary_ip4",
86        "primary_ip6",
87        "role",
88        "vlan",
89        "vrf",
90        "nat_inside",
91        "nat_outside",
92        "region",
93        "untagged_vlan",
94        "tagged_vlans",
95        "tenant",
96    ]
97)
98
99DEVICE_STATUS = dict(offline=0, active=1, planned=2, staged=3, failed=4, inventory=5)
100
101IP_ADDRESS_STATUS = dict(active=1, reserved=2, deprecated=3, dhcp=5)
102
103IP_ADDRESS_ROLE = dict(
104    loopback=10, secondary=20, anycast=30, vip=40, vrrp=41, hsrp=42, glbp=43, carp=44
105)
106
107PREFIX_STATUS = dict(container=0, active=1, reserved=2, deprecated=3)
108
109VLAN_STATUS = dict(active=1, reserved=2, deprecated=3)
110
111SITE_STATUS = dict(active=1, planned=2, retired=4)
112
113INTF_FORM_FACTOR = {
114    "virtual": 0,
115    "link aggregation group (lag)": 200,
116    "100base-tx (10/100me)": 800,
117    "1000base-t (1ge)": 1000,
118    "10gbase-t (10ge)": 1150,
119    "10gbase-cx4 (10ge)": 1170,
120    "gbic (1ge)": 1050,
121    "sfp (1ge)": 1100,
122    "sfp+ (10ge)": 1200,
123    "xfp (10ge)": 1300,
124    "xenpak (10ge)": 1310,
125    "x2 (10ge)": 1320,
126    "sfp28 (25ge)": 1350,
127    "qsfp+ (40ge)": 1400,
128    "cfp (100ge)": 1500,
129    "cfp2 (100ge)": 1510,
130    "cfp2 (200ge)": 1650,
131    "cfp4 (100ge)": 1520,
132    "cisco cpak (100ge)": 1550,
133    "qsfp28 (100ge)": 1600,
134    "qsfp56 (200ge)": 1700,
135    "qsfp-dd (400ge)": 1750,
136    "ieee 802.11a": 2600,
137    "ieee 802.11b/g": 2610,
138    "ieee 802.11n": 2620,
139    "ieee 802.11ac": 2630,
140    "ieee 802.11ad": 2640,
141    "gsm": 2810,
142    "cdma": 2820,
143    "lte": 2830,
144    "oc-3/stm-1": 6100,
145    "oc-12/stm-4": 6200,
146    "oc-48/stm-16": 6300,
147    "oc-192/stm-64": 6400,
148    "oc-768/stm-256": 6500,
149    "oc-1920/stm-640": 6600,
150    "oc-3840/stm-1234": 6700,
151    "sfp (1gfc)": 3010,
152    "sfp (2gfc)": 3020,
153    "sfp (4gfc)": 3040,
154    "sfp+ (8gfc)": 3080,
155    "sfp+ (16gfc)": 3160,
156    "sfp28 (32gfc)": 3320,
157    "qsfp28 (128gfc)": 3400,
158    "t1 (1.544 mbps)": 4000,
159    "e1 (2.048 mbps)": 4010,
160    "t3 (45 mbps)": 4040,
161    "e3 (34 mbps)": 4050,
162    "cisco stackwise": 5000,
163    "cisco stackwise plus": 5050,
164    "cisco flexstack": 5100,
165    "cisco flexstack plus": 5150,
166    "juniper vcp": 5200,
167    "extreme summitstack": 5300,
168    "extreme summitstack-128": 5310,
169    "extreme summitstack-256": 5320,
170    "extreme summitstack-512": 5330,
171    "other": 32767,
172}
173
174INTF_MODE = {"access": 100, "tagged": 200, "tagged all": 300}
175
176ALLOWED_QUERY_PARAMS = {
177    "interface": set(["name", "device"]),
178    "lag": set(["name"]),
179    "nat_inside": set(["vrf", "address"]),
180    "vlan": set(["name", "site", "vlan_group", "tenant"]),
181    "untagged_vlan": set(["name", "site", "vlan_group", "tenant"]),
182    "tagged_vlans": set(["name", "site", "vlan_group", "tenant"]),
183}
184
185QUERY_PARAMS_IDS = set(["vrf", "site", "vlan_group", "tenant"])
186
187
188def _build_diff(before=None, after=None):
189    return {"before": before, "after": after}
190
191
192def create_netbox_object(nb_endpoint, data, check_mode):
193    """Create a Netbox object.
194    :returns tuple(serialized_nb_obj, diff): tuple of the serialized created
195    Netbox object and the Ansible diff.
196    """
197    if check_mode:
198        serialized_nb_obj = data
199    else:
200        nb_obj = nb_endpoint.create(data)
201        try:
202            serialized_nb_obj = nb_obj.serialize()
203        except AttributeError:
204            serialized_nb_obj = nb_obj
205
206    diff = _build_diff(before={"state": "absent"}, after={"state": "present"})
207    return serialized_nb_obj, diff
208
209
210def delete_netbox_object(nb_obj, check_mode):
211    """Delete a Netbox object.
212    :returns tuple(serialized_nb_obj, diff): tuple of the serialized deleted
213    Netbox object and the Ansible diff.
214    """
215    if not check_mode:
216        nb_obj.delete()
217
218    diff = _build_diff(before={"state": "present"}, after={"state": "absent"})
219    return nb_obj.serialize(), diff
220
221
222def update_netbox_object(nb_obj, data, check_mode):
223    """Update a Netbox object.
224    :returns tuple(serialized_nb_obj, diff): tuple of the serialized updated
225    Netbox object and the Ansible diff.
226    """
227    serialized_nb_obj = nb_obj.serialize()
228    updated_obj = serialized_nb_obj.copy()
229    updated_obj.update(data)
230    if serialized_nb_obj == updated_obj:
231        return serialized_nb_obj, None
232    else:
233        data_before, data_after = {}, {}
234        for key in data:
235            if serialized_nb_obj[key] != updated_obj[key]:
236                data_before[key] = serialized_nb_obj[key]
237                data_after[key] = updated_obj[key]
238
239        if not check_mode:
240            nb_obj.update(data)
241            updated_obj = nb_obj.serialize()
242
243        diff = _build_diff(before=data_before, after=data_after)
244        return updated_obj, diff
245
246
247def _get_query_param_id(nb, match, child):
248    endpoint = CONVERT_TO_ID[match]
249    app = find_app(endpoint)
250    nb_app = getattr(nb, app)
251    nb_endpoint = getattr(nb_app, endpoint)
252    result = nb_endpoint.get(**{QUERY_TYPES.get(match): child[match]})
253    if result:
254        return result.id
255    else:
256        return child
257
258
259def find_app(endpoint):
260    for k, v in API_APPS_ENDPOINTS.items():
261        if endpoint in v:
262            nb_app = k
263    return nb_app
264
265
266def build_query_params(nb, parent, module_data, child):
267    query_dict = dict()
268    query_params = ALLOWED_QUERY_PARAMS.get(parent)
269    matches = query_params.intersection(set(child.keys()))
270    for match in matches:
271        if match in QUERY_PARAMS_IDS:
272            value = _get_query_param_id(nb, match, child)
273            query_dict.update({match + "_id": value})
274        else:
275            value = child.get(match)
276            query_dict.update({match: value})
277
278    if parent == "lag":
279        query_dict.update({"form_factor": 200})
280        if isinstance(module_data["device"], int):
281            query_dict.update({"device_id": module_data["device"]})
282        else:
283            query_dict.update({"device": module_data["device"]})
284
285    return query_dict
286
287
288def find_ids(nb, data):
289    for k, v in data.items():
290        if k in CONVERT_TO_ID:
291            endpoint = CONVERT_TO_ID[k]
292            search = v
293            app = find_app(endpoint)
294            nb_app = getattr(nb, app)
295            nb_endpoint = getattr(nb_app, endpoint)
296
297            if isinstance(v, dict):
298                query_params = build_query_params(nb, k, data, v)
299                query_id = nb_endpoint.get(**query_params)
300
301            elif isinstance(v, list):
302                id_list = list()
303                for index in v:
304                    norm_data = normalize_data(index)
305                    temp_dict = build_query_params(nb, k, data, norm_data)
306                    query_id = nb_endpoint.get(**temp_dict)
307                    if query_id:
308                        id_list.append(query_id.id)
309                    else:
310                        return ValueError("%s not found" % (index))
311
312            else:
313                try:
314                    query_id = nb_endpoint.get(**{QUERY_TYPES.get(k, "q"): search})
315                except ValueError:
316                    raise ValueError(
317                        "Multiple results found while searching for key: %s" % (k)
318                    )
319
320            if isinstance(v, list):
321                data[k] = id_list
322            elif query_id:
323                data[k] = query_id.id
324            elif k in NO_DEFAULT_ID:
325                pass
326            else:
327                raise ValueError("Could not resolve id of %s: %s" % (k, v))
328
329    return data
330
331
332def normalize_data(data):
333    for k, v in data.items():
334        if isinstance(v, dict):
335            for subk, subv in v.items():
336                sub_data_type = QUERY_TYPES.get(subk, "q")
337                if sub_data_type == "slug":
338                    if "-" in subv:
339                        data[k][subk] = subv.replace(" ", "").lower()
340                    elif " " in subv:
341                        data[k][subk] = subv.replace(" ", "-").lower()
342                    else:
343                        data[k][subk] = subv.lower()
344        else:
345            data_type = QUERY_TYPES.get(k, "q")
346            if data_type == "slug":
347                if "-" in v:
348                    data[k] = v.replace(" ", "").lower()
349                elif " " in v:
350                    data[k] = v.replace(" ", "-").lower()
351                else:
352                    data[k] = v.lower()
353            elif data_type == "timezone":
354                if " " in v:
355                    data[k] = v.replace(" ", "_")
356
357    return data
358