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