1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) <mikhail.yohman@gmail.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14DOCUMENTATION = r''' 15--- 16module: netbox_ip_address 17short_description: Creates or removes IP addresses from Netbox 18description: 19 - Creates or removes IP addresses from Netbox 20notes: 21 - Tags should be defined as a YAML list 22 - This should be ran with connection C(local) and hosts C(localhost) 23author: 24 - Mikhail Yohman (@FragmentedPacket) 25 - Anthony Ruhier (@Anthony25) 26requirements: 27 - pynetbox 28version_added: '2.8' 29options: 30 netbox_url: 31 description: 32 - URL of the Netbox instance resolvable by Ansible control host 33 required: true 34 netbox_token: 35 description: 36 - The token created within Netbox to authorize API access 37 required: true 38 data: 39 description: 40 - Defines the IP address configuration 41 suboptions: 42 family: 43 description: 44 - Specifies with address family the IP address belongs to 45 choices: 46 - 4 47 - 6 48 address: 49 description: 50 - Required if state is C(present) 51 prefix: 52 description: 53 - | 54 With state C(present), if an interface is given, it will ensure 55 that an IP inside this prefix (and vrf, if given) is attached 56 to this interface. Otherwise, it will get the next available IP 57 of this prefix and attach it. 58 With state C(new), it will force to get the next available IP in 59 this prefix. If an interface is given, it will also force to attach 60 it. 61 Required if state is C(present) or C(new) when no address is given. 62 Unused if an address is specified. 63 vrf: 64 description: 65 - VRF that IP address is associated with 66 tenant: 67 description: 68 - The tenant that the device will be assigned to 69 status: 70 description: 71 - The status of the IP address 72 choices: 73 - Active 74 - Reserved 75 - Deprecated 76 - DHCP 77 role: 78 description: 79 - The role of the IP address 80 choices: 81 - Loopback 82 - Secondary 83 - Anycast 84 - VIP 85 - VRRP 86 - HSRP 87 - GLBP 88 - CARP 89 interface: 90 description: 91 - | 92 The name and device of the interface that the IP address should be assigned to 93 Required if state is C(present) and a prefix specified. 94 description: 95 description: 96 - The description of the interface 97 nat_inside: 98 description: 99 - The inside IP address this IP is assigned to 100 tags: 101 description: 102 - Any tags that the IP address may need to be associated with 103 custom_fields: 104 description: 105 - must exist in Netbox 106 required: true 107 state: 108 description: 109 - | 110 Use C(present), C(new) or C(absent) for adding, force adding or removing. 111 C(present) will check if the IP is already created, and return it if 112 true. C(new) will force to create it anyway (useful for anycasts, for 113 example). 114 choices: [ absent, new, present ] 115 default: present 116 validate_certs: 117 description: 118 - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. 119 default: 'yes' 120 type: bool 121''' 122 123EXAMPLES = r''' 124- name: "Test Netbox IP address module" 125 connection: local 126 hosts: localhost 127 gather_facts: False 128 129 tasks: 130 - name: Create IP address within Netbox with only required information 131 netbox_ip_address: 132 netbox_url: http://netbox.local 133 netbox_token: thisIsMyToken 134 data: 135 address: 192.168.1.10 136 state: present 137 - name: Force to create (even if it already exists) the IP 138 netbox_ip_address: 139 netbox_url: http://netbox.local 140 netbox_token: thisIsMyToken 141 data: 142 address: 192.168.1.10 143 state: new 144 - name: Get a new available IP inside 192.168.1.0/24 145 netbox_ip_address: 146 netbox_url: http://netbox.local 147 netbox_token: thisIsMyToken 148 data: 149 prefix: 192.168.1.0/24 150 state: new 151 - name: Delete IP address within netbox 152 netbox_ip_address: 153 netbox_url: http://netbox.local 154 netbox_token: thisIsMyToken 155 data: 156 address: 192.168.1.10 157 state: absent 158 - name: Create IP address with several specified options 159 netbox_ip_address: 160 netbox_url: http://netbox.local 161 netbox_token: thisIsMyToken 162 data: 163 family: 4 164 address: 192.168.1.20 165 vrf: Test 166 tenant: Test Tenant 167 status: Reserved 168 role: Loopback 169 description: Test description 170 tags: 171 - Schnozzberry 172 state: present 173 - name: Create IP address and assign a nat_inside IP 174 netbox_ip_address: 175 netbox_url: http://netbox.local 176 netbox_token: thisIsMyToken 177 data: 178 family: 4 179 address: 192.168.1.30 180 vrf: Test 181 nat_inside: 182 address: 192.168.1.20 183 vrf: Test 184 interface: 185 name: GigabitEthernet1 186 device: test100 187 - name: Ensure that an IP inside 192.168.1.0/24 is attached to GigabitEthernet1 188 netbox_ip_address: 189 netbox_url: http://netbox.local 190 netbox_token: thisIsMyToken 191 data: 192 prefix: 192.168.1.0/24 193 vrf: Test 194 interface: 195 name: GigabitEthernet1 196 device: test100 197 state: present 198 - name: Attach a new available IP of 192.168.1.0/24 to GigabitEthernet1 199 netbox_ip_address: 200 netbox_url: http://netbox.local 201 netbox_token: thisIsMyToken 202 data: 203 prefix: 192.168.1.0/24 204 vrf: Test 205 interface: 206 name: GigabitEthernet1 207 device: test100 208 state: new 209''' 210 211RETURN = r''' 212ip_address: 213 description: Serialized object as created or already existent within Netbox 214 returned: on creation 215 type: dict 216msg: 217 description: Message indicating failure or info about what has been achieved 218 returned: always 219 type: str 220''' 221 222import json 223import traceback 224 225from ansible.module_utils.basic import AnsibleModule, missing_required_lib 226from ansible.module_utils.net_tools.netbox.netbox_utils import ( 227 find_ids, 228 normalize_data, 229 create_netbox_object, 230 delete_netbox_object, 231 update_netbox_object, 232 IP_ADDRESS_ROLE, 233 IP_ADDRESS_STATUS 234) 235from ansible.module_utils.compat import ipaddress 236from ansible.module_utils._text import to_text 237 238 239PYNETBOX_IMP_ERR = None 240try: 241 import pynetbox 242 HAS_PYNETBOX = True 243except ImportError: 244 PYNETBOX_IMP_ERR = traceback.format_exc() 245 HAS_PYNETBOX = False 246 247 248def main(): 249 ''' 250 Main entry point for module execution 251 ''' 252 argument_spec = dict( 253 netbox_url=dict(type="str", required=True), 254 netbox_token=dict(type="str", required=True, no_log=True), 255 data=dict(type="dict", required=True), 256 state=dict(required=False, default='present', choices=['present', 'absent', 'new']), 257 validate_certs=dict(type="bool", default=True) 258 ) 259 260 global module 261 module = AnsibleModule(argument_spec=argument_spec, 262 supports_check_mode=True) 263 264 # Fail module if pynetbox is not installed 265 if not HAS_PYNETBOX: 266 module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) 267 268 # Assign variables to be used with module 269 changed = False 270 app = 'ipam' 271 endpoint = 'ip_addresses' 272 url = module.params["netbox_url"] 273 token = module.params["netbox_token"] 274 data = module.params["data"] 275 state = module.params["state"] 276 validate_certs = module.params["validate_certs"] 277 278 # Attempt to create Netbox API object 279 try: 280 nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) 281 except Exception: 282 module.fail_json(msg="Failed to establish connection to Netbox API") 283 try: 284 nb_app = getattr(nb, app) 285 except AttributeError: 286 module.fail_json(msg="Incorrect application specified: %s" % (app)) 287 288 nb_endpoint = getattr(nb_app, endpoint) 289 norm_data = normalize_data(data) 290 try: 291 norm_data = _check_and_adapt_data(nb, norm_data) 292 if state in ("new", "present"): 293 return _handle_state_new_present( 294 module, state, nb_app, nb_endpoint, norm_data 295 ) 296 elif state == "absent": 297 return module.exit_json( 298 **ensure_ip_address_absent(nb_endpoint, norm_data) 299 ) 300 else: 301 return module.fail_json(msg="Invalid state %s" % state) 302 except pynetbox.RequestError as e: 303 return module.fail_json(msg=json.loads(e.error)) 304 except ValueError as e: 305 return module.fail_json(msg=str(e)) 306 307 308def _check_and_adapt_data(nb, data): 309 data = find_ids(nb, data) 310 311 if data.get("vrf") and not isinstance(data["vrf"], int): 312 raise ValueError( 313 "%s does not exist - Please create VRF" % (data["vrf"]) 314 ) 315 if data.get("status"): 316 data["status"] = IP_ADDRESS_STATUS.get(data["status"].lower()) 317 if data.get("role"): 318 data["role"] = IP_ADDRESS_ROLE.get(data["role"].lower()) 319 320 return data 321 322 323def _handle_state_new_present(module, state, nb_app, nb_endpoint, data): 324 if data.get("address"): 325 if state == "present": 326 return module.exit_json( 327 **ensure_ip_address_present(nb_endpoint, data) 328 ) 329 elif state == "new": 330 return module.exit_json( 331 **create_ip_address(nb_endpoint, data) 332 ) 333 else: 334 if state == "present": 335 return module.exit_json( 336 **ensure_ip_in_prefix_present_on_netif( 337 nb_app, nb_endpoint, data 338 ) 339 ) 340 elif state == "new": 341 return module.exit_json( 342 **get_new_available_ip_address(nb_app, data) 343 ) 344 345 346def ensure_ip_address_present(nb_endpoint, data): 347 """ 348 :returns dict(ip_address, msg, changed): dictionary resulting of the request, 349 where 'ip_address' is the serialized ip fetched or newly created in Netbox 350 """ 351 if not isinstance(data, dict): 352 changed = False 353 return {"msg": data, "changed": changed} 354 355 try: 356 nb_addr = _search_ip(nb_endpoint, data) 357 except ValueError: 358 return _error_multiple_ip_results(data) 359 360 result = {} 361 if not nb_addr: 362 return create_ip_address(nb_endpoint, data) 363 else: 364 ip_addr, diff = update_netbox_object(nb_addr, data, module.check_mode) 365 if ip_addr is False: 366 module.fail_json( 367 msg="Request failed, couldn't update IP: %s" % (data["address"]) 368 ) 369 if diff: 370 msg = "IP Address %s updated" % (data["address"]) 371 changed = True 372 result["diff"] = diff 373 else: 374 ip_addr = nb_addr.serialize() 375 changed = False 376 msg = "IP Address %s already exists" % (data["address"]) 377 378 return {"ip_address": ip_addr, "msg": msg, "changed": changed} 379 380 381def _search_ip(nb_endpoint, data): 382 get_query_params = {"address": data["address"]} 383 if data.get("vrf"): 384 get_query_params["vrf_id"] = data["vrf"] 385 386 ip_addr = nb_endpoint.get(**get_query_params) 387 return ip_addr 388 389 390def _error_multiple_ip_results(data): 391 changed = False 392 if "vrf" in data: 393 return {"msg": "Returned more than result", "changed": changed} 394 else: 395 return { 396 "msg": "Returned more than one result - Try specifying VRF.", 397 "changed": changed 398 } 399 400 401def create_ip_address(nb_endpoint, data): 402 if not isinstance(data, dict): 403 changed = False 404 return {"msg": data, "changed": changed} 405 406 ip_addr, diff = create_netbox_object(nb_endpoint, data, module.check_mode) 407 changed = True 408 msg = "IP Addresses %s created" % (data["address"]) 409 410 return {"ip_address": ip_addr, "msg": msg, "changed": changed, "diff": diff} 411 412 413def ensure_ip_in_prefix_present_on_netif(nb_app, nb_endpoint, data): 414 """ 415 :returns dict(ip_address, msg, changed): dictionary resulting of the request, 416 where 'ip_address' is the serialized ip fetched or newly created in Netbox 417 """ 418 if not isinstance(data, dict): 419 changed = False 420 return {"msg": data, "changed": changed} 421 422 if not data.get("interface") or not data.get("prefix"): 423 raise ValueError("A prefix and interface are required") 424 425 get_query_params = { 426 "interface_id": data["interface"], "parent": data["prefix"], 427 } 428 if data.get("vrf"): 429 get_query_params["vrf_id"] = data["vrf"] 430 431 attached_ips = nb_endpoint.filter(**get_query_params) 432 if attached_ips: 433 ip_addr = attached_ips[-1].serialize() 434 changed = False 435 msg = "IP Address %s already attached" % (ip_addr["address"]) 436 437 return {"ip_address": ip_addr, "msg": msg, "changed": changed} 438 else: 439 return get_new_available_ip_address(nb_app, data) 440 441 442def get_new_available_ip_address(nb_app, data): 443 prefix_query = {"prefix": data["prefix"]} 444 if data.get("vrf"): 445 prefix_query["vrf_id"] = data["vrf"] 446 447 result = {} 448 prefix = nb_app.prefixes.get(**prefix_query) 449 if not prefix: 450 changed = False 451 msg = "%s does not exist - please create first" % (data["prefix"]) 452 return {"msg": msg, "changed": changed} 453 elif prefix.available_ips.list(): 454 ip_addr, diff = create_netbox_object(prefix.available_ips, data, module.check_mode) 455 changed = True 456 msg = "IP Addresses %s created" % (ip_addr["address"]) 457 result["diff"] = diff 458 else: 459 changed = False 460 msg = "No available IPs available within %s" % (data['prefix']) 461 return {"msg": msg, "changed": changed} 462 463 result.update({"ip_address": ip_addr, "msg": msg, "changed": changed}) 464 return result 465 466 467def _get_prefix_id(nb_app, prefix, vrf_id=None): 468 ipaddr_prefix = ipaddress.ip_network(prefix) 469 network = to_text(ipaddr_prefix.network_address) 470 mask = ipaddr_prefix.prefixlen 471 472 prefix_query_params = { 473 "prefix": network, 474 "mask_length": mask 475 } 476 if vrf_id: 477 prefix_query_params["vrf_id"] = vrf_id 478 479 prefix_id = nb_app.prefixes.get(prefix_query_params) 480 if not prefix_id: 481 if vrf_id: 482 raise ValueError("Prefix %s does not exist in VRF %s - Please create it" % (prefix, vrf_id)) 483 else: 484 raise ValueError("Prefix %s does not exist - Please create it" % (prefix)) 485 486 return prefix_id 487 488 489def ensure_ip_address_absent(nb_endpoint, data): 490 """ 491 :returns dict(msg, changed) 492 """ 493 if not isinstance(data, dict): 494 changed = False 495 return {"msg": data, "changed": changed} 496 497 try: 498 ip_addr = _search_ip(nb_endpoint, data) 499 except ValueError: 500 return _error_multiple_ip_results(data) 501 502 result = {} 503 if ip_addr: 504 dummy, diff = delete_netbox_object(ip_addr, module.check_mode) 505 changed = True 506 msg = "IP Address %s deleted" % (data["address"]) 507 result["diff"] = diff 508 else: 509 changed = False 510 msg = "IP Address %s already absent" % (data["address"]) 511 512 result.update({"msg": msg, "changed": changed}) 513 return result 514 515 516if __name__ == "__main__": 517 main() 518