1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# (c) 2015, Patrick F. Marques <patrickfmarques@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 9__metaclass__ = type 10 11 12DOCUMENTATION = r""" 13--- 14module: digital_ocean_floating_ip 15short_description: Manage DigitalOcean Floating IPs 16description: 17 - Create/delete/assign a floating IP. 18author: "Patrick Marques (@pmarques)" 19options: 20 state: 21 description: 22 - Indicate desired state of the target. 23 default: present 24 choices: ['present', 'absent'] 25 type: str 26 ip: 27 description: 28 - Public IP address of the Floating IP. Used to remove an IP 29 type: str 30 aliases: ['id'] 31 region: 32 description: 33 - The region that the Floating IP is reserved to. 34 type: str 35 droplet_id: 36 description: 37 - The Droplet that the Floating IP has been assigned to. 38 type: str 39 oauth_token: 40 description: 41 - DigitalOcean OAuth token. 42 required: true 43 type: str 44 timeout: 45 description: 46 - Floating IP creation timeout. 47 type: int 48 default: 30 49 validate_certs: 50 description: 51 - If set to C(no), the SSL certificates will not be validated. 52 - This should only set to C(no) used on personally controlled sites using self-signed certificates. 53 type: bool 54 default: true 55notes: 56 - Version 2 of DigitalOcean API is used. 57requirements: 58 - "python >= 2.6" 59""" 60 61 62EXAMPLES = r""" 63- name: "Create a Floating IP in region lon1" 64 community.digitalocean.digital_ocean_floating_ip: 65 state: present 66 region: lon1 67 68- name: "Create a Floating IP assigned to Droplet ID 123456" 69 community.digitalocean.digital_ocean_floating_ip: 70 state: present 71 droplet_id: 123456 72 73- name: "Delete a Floating IP with ip 1.2.3.4" 74 community.digitalocean.digital_ocean_floating_ip: 75 state: absent 76 ip: "1.2.3.4" 77 78""" 79 80 81RETURN = r""" 82# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#floating-ips 83data: 84 description: a DigitalOcean Floating IP resource 85 returned: success and no resource constraint 86 type: dict 87 sample: { 88 "action": { 89 "id": 68212728, 90 "status": "in-progress", 91 "type": "assign_ip", 92 "started_at": "2015-10-15T17:45:44Z", 93 "completed_at": null, 94 "resource_id": 758603823, 95 "resource_type": "floating_ip", 96 "region": { 97 "name": "New York 3", 98 "slug": "nyc3", 99 "sizes": [ 100 "512mb", 101 "1gb", 102 "2gb", 103 "4gb", 104 "8gb", 105 "16gb", 106 "32gb", 107 "48gb", 108 "64gb" 109 ], 110 "features": [ 111 "private_networking", 112 "backups", 113 "ipv6", 114 "metadata" 115 ], 116 "available": true 117 }, 118 "region_slug": "nyc3" 119 } 120 } 121""" 122 123import json 124import time 125 126from ansible.module_utils.basic import AnsibleModule 127from ansible.module_utils.basic import env_fallback 128from ansible.module_utils.urls import fetch_url 129 130 131class Response(object): 132 def __init__(self, resp, info): 133 self.body = None 134 if resp: 135 self.body = resp.read() 136 self.info = info 137 138 @property 139 def json(self): 140 if not self.body: 141 if "body" in self.info: 142 return json.loads(self.info["body"]) 143 return None 144 try: 145 return json.loads(self.body) 146 except ValueError: 147 return None 148 149 @property 150 def status_code(self): 151 return self.info["status"] 152 153 154class Rest(object): 155 def __init__(self, module, headers): 156 self.module = module 157 self.headers = headers 158 self.baseurl = "https://api.digitalocean.com/v2" 159 160 def _url_builder(self, path): 161 if path[0] == "/": 162 path = path[1:] 163 return "%s/%s" % (self.baseurl, path) 164 165 def send(self, method, path, data=None, headers=None): 166 url = self._url_builder(path) 167 data = self.module.jsonify(data) 168 timeout = self.module.params["timeout"] 169 170 resp, info = fetch_url( 171 self.module, 172 url, 173 data=data, 174 headers=self.headers, 175 method=method, 176 timeout=timeout, 177 ) 178 179 # Exceptions in fetch_url may result in a status -1, the ensures a 180 if info["status"] == -1: 181 self.module.fail_json(msg=info["msg"]) 182 183 return Response(resp, info) 184 185 def get(self, path, data=None, headers=None): 186 return self.send("GET", path, data, headers) 187 188 def put(self, path, data=None, headers=None): 189 return self.send("PUT", path, data, headers) 190 191 def post(self, path, data=None, headers=None): 192 return self.send("POST", path, data, headers) 193 194 def delete(self, path, data=None, headers=None): 195 return self.send("DELETE", path, data, headers) 196 197 198def wait_action(module, rest, ip, action_id, timeout=10): 199 end_time = time.time() + 10 200 while time.time() < end_time: 201 response = rest.get("floating_ips/{0}/actions/{1}".format(ip, action_id)) 202 status_code = response.status_code 203 status = response.json["action"]["status"] 204 # TODO: check status_code == 200? 205 if status == "completed": 206 return True 207 elif status == "errored": 208 module.fail_json( 209 msg="Floating ip action error [ip: {0}: action: {1}]".format( 210 ip, action_id 211 ), 212 data=json, 213 ) 214 215 module.fail_json( 216 msg="Floating ip action timeout [ip: {0}: action: {1}]".format(ip, action_id), 217 data=json, 218 ) 219 220 221def core(module): 222 api_token = module.params["oauth_token"] 223 state = module.params["state"] 224 ip = module.params["ip"] 225 droplet_id = module.params["droplet_id"] 226 227 rest = Rest( 228 module, 229 { 230 "Authorization": "Bearer {0}".format(api_token), 231 "Content-type": "application/json", 232 }, 233 ) 234 235 if state in ("present"): 236 if droplet_id is not None and module.params["ip"] is not None: 237 # Lets try to associate the ip to the specified droplet 238 associate_floating_ips(module, rest) 239 else: 240 create_floating_ips(module, rest) 241 242 elif state in ("absent"): 243 response = rest.delete("floating_ips/{0}".format(ip)) 244 status_code = response.status_code 245 json_data = response.json 246 if status_code == 204: 247 module.exit_json(changed=True) 248 elif status_code == 404: 249 module.exit_json(changed=False) 250 else: 251 module.exit_json(changed=False, data=json_data) 252 253 254def get_floating_ip_details(module, rest): 255 ip = module.params["ip"] 256 257 response = rest.get("floating_ips/{0}".format(ip)) 258 status_code = response.status_code 259 json_data = response.json 260 if status_code == 200: 261 return json_data["floating_ip"] 262 else: 263 module.fail_json( 264 msg="Error assigning floating ip [{0}: {1}]".format( 265 status_code, json_data["message"] 266 ), 267 region=module.params["region"], 268 ) 269 270 271def assign_floating_id_to_droplet(module, rest): 272 ip = module.params["ip"] 273 274 payload = { 275 "type": "assign", 276 "droplet_id": module.params["droplet_id"], 277 } 278 279 response = rest.post("floating_ips/{0}/actions".format(ip), data=payload) 280 status_code = response.status_code 281 json_data = response.json 282 if status_code == 201: 283 wait_action(module, rest, ip, json_data["action"]["id"]) 284 285 module.exit_json(changed=True, data=json_data) 286 else: 287 module.fail_json( 288 msg="Error creating floating ip [{0}: {1}]".format( 289 status_code, json_data["message"] 290 ), 291 region=module.params["region"], 292 ) 293 294 295def associate_floating_ips(module, rest): 296 floating_ip = get_floating_ip_details(module, rest) 297 droplet = floating_ip["droplet"] 298 299 # TODO: If already assigned to a droplet verify if is one of the specified as valid 300 if droplet is not None and str(droplet["id"]) in [module.params["droplet_id"]]: 301 module.exit_json(changed=False) 302 else: 303 assign_floating_id_to_droplet(module, rest) 304 305 306def create_floating_ips(module, rest): 307 payload = {} 308 309 if module.params["region"] is not None: 310 payload["region"] = module.params["region"] 311 if module.params["droplet_id"] is not None: 312 payload["droplet_id"] = module.params["droplet_id"] 313 314 # Get existing floating IPs 315 response = rest.get("floating_ips/") 316 status_code = response.status_code 317 json_data = response.json 318 319 # Exit unchanged if any of them are assigned to this Droplet already 320 if status_code == 200: 321 floating_ips = json_data.get("floating_ips", []) 322 if len(floating_ips) != 0: 323 for floating_ip in floating_ips: 324 droplet = floating_ip.get("droplet", None) 325 if droplet is not None: 326 droplet_id = droplet.get("id", None) 327 if droplet_id is not None: 328 if str(droplet_id) == module.params["droplet_id"]: 329 ip = floating_ip.get("ip", None) 330 if ip is not None: 331 module.exit_json( 332 changed=False, data={"floating_ip": ip} 333 ) 334 else: 335 module.fail_json( 336 changed=False, 337 msg="Unexpected error querying floating ip", 338 ) 339 340 response = rest.post("floating_ips", data=payload) 341 status_code = response.status_code 342 json_data = response.json 343 if status_code == 202: 344 module.exit_json(changed=True, data=json_data) 345 else: 346 module.fail_json( 347 msg="Error creating floating ip [{0}: {1}]".format( 348 status_code, json_data["message"] 349 ), 350 region=module.params["region"], 351 ) 352 353 354def main(): 355 module = AnsibleModule( 356 argument_spec=dict( 357 state=dict(choices=["present", "absent"], default="present"), 358 ip=dict(aliases=["id"], required=False), 359 region=dict(required=False), 360 droplet_id=dict(required=False), 361 oauth_token=dict( 362 no_log=True, 363 # Support environment variable for DigitalOcean OAuth Token 364 fallback=( 365 env_fallback, 366 ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"], 367 ), 368 required=True, 369 ), 370 validate_certs=dict(type="bool", default=True), 371 timeout=dict(type="int", default=30), 372 ), 373 required_if=[("state", "delete", ["ip"])], 374 mutually_exclusive=[["region", "droplet_id"]], 375 ) 376 377 core(module) 378 379 380if __name__ == "__main__": 381 main() 382