def core(module): state = module.params['state'] name = module.params['name'] rest = DigitalOceanHelper(module) results = dict(changed=False) response = rest.get('certificates') status_code = response.status_code resp_json = response.json if status_code != 200: module.fail_json(msg="Failed to retrieve certificates for DigitalOcean") if state == 'present': for cert in resp_json['certificates']: if cert['name'] == name: module.fail_json(msg="Certificate name %s already exists" % name) # Certificate does not exist, let us create it cert_data = dict(name=name, private_key=module.params['private_key'], leaf_certificate=module.params['leaf_certificate']) if module.params['certificate_chain'] is not None: cert_data.update(certificate_chain=module.params['certificate_chain']) response = rest.post("certificates", data=cert_data) status_code = response.status_code if status_code == 500: module.fail_json(msg="Failed to upload certificates as the certificates are malformed.") resp_json = response.json if status_code == 201: results.update(changed=True, response=resp_json) elif status_code == 422: results.update(changed=False, response=resp_json) elif state == 'absent': cert_id_del = None for cert in resp_json['certificates']: if cert['name'] == name: cert_id_del = cert['id'] if cert_id_del is not None: url = "certificates/{0}".format(cert_id_del) response = rest.delete(url) if response.status_code == 204: results.update(changed=True) else: results.update(changed=False) else: module.fail_json(msg="Failed to find certificate %s" % name) module.exit_json(**results)
class DOFirewall(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.name = self.module.params.get('name') self.baseurl = 'firewalls' self.firewalls = self.get_firewalls() def get_firewalls(self): base_url = self.baseurl + "?" response = self.rest.get("%s" % base_url) status_code = response.status_code if status_code != 200: self.module.fail_json( msg="Failed to retrieve firewalls from DigitalOcean") return self.rest.get_paginated_data(base_url=base_url, data_key_name='firewalls') def get_firewall_by_name(self): rule = {} for firewall in self.firewalls: if firewall['name'] == self.name: rule.update(firewall) return rule return None def ordered(self, obj): if isinstance(obj, dict): return sorted((k, self.ordered(v)) for k, v in obj.items()) if isinstance(obj, list): return sorted(self.ordered(x) for x in obj) else: return obj def fill_protocol_defaults(self, obj): if obj.get('protocol') is None: obj['protocol'] = 'tcp' return obj def fill_source_and_destination_defaults_inner(self, obj): addresses = obj.get('addresses') if addresses is None: addresses = [] droplet_ids = obj.get('droplet_ids') if droplet_ids is None: droplet_ids = [] load_balancer_uids = obj.get('load_balancer_uids') if load_balancer_uids is None: load_balancer_uids = [] tags = obj.get('tags') if tags is None: tags = [] data = { "addresses": addresses, "droplet_ids": droplet_ids, "load_balancer_uids": load_balancer_uids, "tags": tags } return data def fill_sources_and_destinations_defaults(self, obj, prop): value = obj.get(prop) if value is None: value = {} else: value = self.fill_source_and_destination_defaults_inner(value) obj[prop] = value return obj def fill_data_defaults(self, obj): inbound_rules = obj.get('inbound_rules') if inbound_rules is None: inbound_rules = [] else: inbound_rules = [ self.fill_protocol_defaults(x) for x in inbound_rules ] inbound_rules = [ self.fill_sources_and_destinations_defaults(x, 'sources') for x in inbound_rules ] outbound_rules = obj.get('outbound_rules') if outbound_rules is None: outbound_rules = [] else: outbound_rules = [ self.fill_protocol_defaults(x) for x in outbound_rules ] outbound_rules = [ self.fill_sources_and_destinations_defaults(x, 'destinations') for x in outbound_rules ] droplet_ids = obj.get('droplet_ids') if droplet_ids is None: droplet_ids = [] tags = obj.get('tags') if tags is None: tags = [] data = { "name": obj.get('name'), "inbound_rules": inbound_rules, "outbound_rules": outbound_rules, "droplet_ids": droplet_ids, "tags": tags } return data def data_to_compare(self, obj): return self.ordered(self.fill_data_defaults(obj)) def update(self, obj, id): if id is None: status_code_success = 202 resp = self.rest.post(path=self.baseurl, data=obj) else: status_code_success = 200 resp = self.rest.put(path=self.baseurl + '/' + id, data=obj) status_code = resp.status_code if status_code != status_code_success: error = resp.json error.update({'status_code': status_code}) error.update({'status_code_success': status_code_success}) self.module.fail_json(msg=error) self.module.exit_json(changed=True, data=resp.json['firewall']) def create(self): rule = self.get_firewall_by_name() data = { "name": self.module.params.get('name'), "inbound_rules": self.module.params.get('inbound_rules'), "outbound_rules": self.module.params.get('outbound_rules'), "droplet_ids": self.module.params.get('droplet_ids'), "tags": self.module.params.get('tags') } if rule is None: self.update(data, None) else: rule_data = { "name": rule.get('name'), "inbound_rules": rule.get('inbound_rules'), "outbound_rules": rule.get('outbound_rules'), "droplet_ids": rule.get('droplet_ids'), "tags": rule.get('tags') } user_data = { "name": data.get('name'), "inbound_rules": data.get('inbound_rules'), "outbound_rules": data.get('outbound_rules'), "droplet_ids": data.get('droplet_ids'), "tags": data.get('tags') } if self.data_to_compare(user_data) == self.data_to_compare( rule_data): self.module.exit_json(changed=False, data=rule) else: self.update(data, rule.get('id')) def destroy(self): rule = self.get_firewall_by_name() if rule is None: self.module.exit_json(changed=False, data="Firewall does not exist") else: endpoint = self.baseurl + '/' + rule['id'] resp = self.rest.delete(path=endpoint) status_code = resp.status_code if status_code != 204: self.module.fail_json(msg="Failed to delete firewall") self.module.exit_json( changed=True, data="Deleted firewall rule: {0} - {1}".format( rule['name'], rule['id']))
def core(module): state = module.params["state"] name = module.params["name"] resource_id = module.params["resource_id"] resource_type = module.params["resource_type"] rest = DigitalOceanHelper(module) response = rest.get("tags/{0}".format(name)) if state == "present": status_code = response.status_code resp_json = response.json changed = False if status_code == 200 and resp_json["tag"]["name"] == name: changed = False else: # Ensure Tag exists response = rest.post("tags", data={"name": name}) status_code = response.status_code resp_json = response.json if status_code == 201: changed = True elif status_code == 422: changed = False else: module.exit_json(changed=False, data=resp_json) if resource_id is None: # No resource defined, we're done. module.exit_json(changed=changed, data=resp_json) else: # Check if resource is already tagged or not found = False url = "{0}?tag_name={1}".format(resource_type, name) if resource_type == "droplet": url = "droplets?tag_name={0}".format(name) response = rest.get(url) status_code = response.status_code resp_json = response.json if status_code == 200: for resource in resp_json["droplets"]: if not found and resource["id"] == int(resource_id): found = True break if not found: # If resource is not tagged, tag a resource url = "tags/{0}/resources".format(name) payload = { "resources": [{ "resource_id": resource_id, "resource_type": resource_type }] } response = rest.post(url, data=payload) if response.status_code == 204: module.exit_json(changed=True) else: module.fail_json( msg="error tagging resource '{0}': {1}".format( resource_id, response.json["message"])) else: # Already tagged resource module.exit_json(changed=False) else: # Unable to find resource specified by user module.fail_json(msg=resp_json["message"]) elif state == "absent": if response.status_code == 200: if resource_id: url = "tags/{0}/resources".format(name) payload = { "resources": [{ "resource_id": resource_id, "resource_type": resource_type }] } response = rest.delete(url, data=payload) else: url = "tags/{0}".format(name) response = rest.delete(url) if response.status_code == 204: module.exit_json(changed=True) else: module.exit_json(changed=False)
class DODroplet(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 120) self.unique_name = self.module.params.pop("unique_name", False) # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.id = None self.name = None self.size = None self.status = None def get_by_id(self, droplet_id): if not droplet_id: return None response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json if json_data is None: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) else: if response.status_code == 200: droplet = json_data.get("droplet", None) if droplet is not None: self.id = droplet.get("id", None) self.name = droplet.get("name", None) self.size = droplet.get("size_slug", None) self.status = droplet.get("status", None) return json_data return None def get_by_name(self, droplet_name): if not droplet_name: return None page = 1 while page is not None: response = self.rest.get("droplets?page={0}".format(page)) json_data = response.json if json_data is None: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) else: if response.status_code == 200: droplets = json_data.get("droplets", []) for droplet in droplets: if droplet.get("name", None) == droplet_name: self.id = droplet.get("id", None) self.name = droplet.get("name", None) self.size = droplet.get("size_slug", None) self.status = droplet.get("status", None) return {"droplet": droplet} if ("links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"]): page += 1 else: page = None return None def get_addresses(self, data): """Expose IP addresses as their own property allowing users extend to additional tasks""" _data = data for k, v in data.items(): setattr(self, k, v) networks = _data["droplet"]["networks"] for network in networks.get("v4", []): if network["type"] == "public": _data["ip_address"] = network["ip_address"] else: _data["private_ipv4_address"] = network["ip_address"] for network in networks.get("v6", []): if network["type"] == "public": _data["ipv6_address"] = network["ip_address"] else: _data["private_ipv6_address"] = network["ip_address"] return _data def get_droplet(self): json_data = self.get_by_id(self.module.params["id"]) if not json_data and self.unique_name: json_data = self.get_by_name(self.module.params["name"]) return json_data def resize_droplet(self, state): """API reference: https://developers.digitalocean.com/documentation/v2/#resize-a-droplet (Must be powered off)""" if self.status == "off": response = self.rest.post( "droplets/{0}/actions".format(self.id), data={ "type": "resize", "disk": self.module.params["resize_disk"], "size": self.module.params["size"], }, ) json_data = response.json if json_data is None: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) else: if response.status_code == 201: if state == "active": self.ensure_power_on(self.id) self.module.exit_json( changed=True, msg="Resized Droplet {0} ({1}) from {2} to {3}".format( self.name, self.id, self.size, self.module.params["size"]), ) else: self.module.fail_json( msg="Resizing Droplet {0} ({1}) failed [HTTP {2}: {3}]" .format( self.name, self.id, response.status_code, response.json.get("message", None), )) else: self.module.fail_json( msg= "Droplet must be off prior to resizing (https://developers.digitalocean.com/documentation/v2/#resize-a-droplet)" ) def create(self, state): json_data = self.get_droplet() droplet_data = None if json_data is not None: droplet = json_data.get("droplet", None) if droplet is not None: droplet_size = droplet.get("size_slug", None) if droplet_size is not None: if droplet_size != self.module.params["size"]: self.resize_droplet(state) droplet_data = self.get_addresses(json_data) # If state is active or inactive, ensure requested and desired power states match droplet = json_data.get("droplet", None) if droplet is not None: droplet_id = droplet.get("id", None) droplet_status = droplet.get("status", None) if droplet_id is not None and droplet_status is not None: if state == "active" and droplet_status != "active": power_on_json_data = self.ensure_power_on(droplet_id) self.module.exit_json( changed=True, data=self.get_addresses(power_on_json_data)) elif state == "inactive" and droplet_status != "off": power_off_json_data = self.ensure_power_off(droplet_id) self.module.exit_json( changed=True, data=self.get_addresses(power_off_json_data)) else: self.module.exit_json(changed=False, data=droplet_data) if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) del request_params["id"] response = self.rest.post("droplets", data=request_params) json_data = response.json if json_data is None: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) else: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) droplet_data = json_data.get("droplet", None) if droplet_data is not None: droplet_id = droplet_data.get("id", None) if droplet_id is not None: if self.wait: if state == "present" or state == "active": json_data = self.ensure_power_on(droplet_id) if state == "inactive": json_data = self.ensure_power_off(droplet_id) droplet_data = self.get_addresses(json_data) else: if state == "inactive": response = self.rest.post( "droplets/{0}/actions".format(droplet_id), data={"type": "power_off"}, ) else: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug") else: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug") self.module.exit_json(changed=True, data=droplet_data) def delete(self): json_data = self.get_droplet() if json_data: if self.module.check_mode: self.module.exit_json(changed=True) response = self.rest.delete("droplets/{0}".format( json_data["droplet"]["id"])) json_data = response.json if response.status_code == 204: self.module.exit_json(changed=True, msg="Droplet deleted") self.module.fail_json(changed=False, msg="Failed to delete droplet") else: self.module.exit_json(changed=False, msg="Droplet not found") def ensure_power_on(self, droplet_id): # Make sure Droplet is active first end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) droplet = json_data.get("droplet", None) if droplet is None: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug (no droplet)", ) droplet_status = droplet.get("status", None) if droplet_status is None: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug (no status)") if droplet_status == "active": break time.sleep(min(10, end_time - time.monotonic())) # Trigger power-on response = self.rest.post("droplets/{0}/actions".format(droplet_id), data={"type": "power_on"}) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) # Save the power-on action action = json_data.get("action", None) action_id = action.get("id", None) if action is None or action_id is None: self.module.fail_json( changed=False, msg= "Unexpected error, please file a bug (no power-on action or id)", ) # Keep checking till it is done or times out end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}/actions/{1}".format( droplet_id, action_id)) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) action = json_data.get("action", None) action_status = action.get("status", None) if action is None or action_status is None: self.module.fail_json( changed=False, msg= "Unexpected error, please file a bug (no action or status)", ) if action_status == "errored": self.module.fail_json( changed=False, msg= "Error status on droplet power on action, please try again or contact DigitalOcean support", ) if action_status == "completed": response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!", ) self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) return json_data time.sleep(min(10, end_time - time.monotonic())) self.module.fail_json(msg="Wait for droplet powering on timeout") def ensure_power_off(self, droplet_id): # Make sure Droplet is active first end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) droplet = json_data.get("droplet", None) if droplet is None: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug (no droplet)", ) droplet_status = droplet.get("status", None) if droplet_status is None: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug (no status)") if droplet_status == "active": break time.sleep(min(10, end_time - time.monotonic())) # Trigger power-off response = self.rest.post("droplets/{0}/actions".format(droplet_id), data={"type": "power_off"}) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) # Save the power-off action action = json_data.get("action", None) action_id = action.get("id", None) if action is None or action_id is None: self.module.fail_json( changed=False, msg= "Unexpected error, please file a bug (no power-off action or id)", ) # Keep checking till it is done or times out end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}/actions/{1}".format( droplet_id, action_id)) json_data = response.json if json_data is not None: if response.status_code >= 400: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json(changed=False, msg=message) else: self.module.fail_json( changed=False, msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds.", ) action = json_data.get("action", None) action_status = action.get("status", None) if action is None or action_status is None: self.module.fail_json( changed=False, msg= "Unexpected error, please file a bug (no action or status)", ) if action_status == "errored": self.module.fail_json( changed=False, msg= "Error status on droplet power off action, please try again or contact DigitalOcean support", ) if action_status == "completed": response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json if response.status_code >= 400: self.module.fail_json(changed=False, msg=json_data["message"]) return json_data time.sleep(min(10, end_time - time.monotonic())) self.module.fail_json(msg="Wait for droplet powering off timeout")
class DOBlockStorage(object): def __init__(self, module): self.module = module self.rest = DigitalOceanHelper(module) def get_key_or_fail(self, k): v = self.module.params[k] if v is None: self.module.fail_json(msg="Unable to load %s" % k) return v def poll_action_for_complete_status(self, action_id): url = "actions/{0}".format(action_id) end_time = time.time() + self.module.params["timeout"] while time.time() < end_time: time.sleep(2) response = self.rest.get(url) status = response.status_code json = response.json if status == 200: if json["action"]["status"] == "completed": return True elif json["action"]["status"] == "errored": raise DOBlockStorageException(json["message"]) raise DOBlockStorageException("Unable to reach api.digitalocean.com") def get_block_storage_by_name(self, volume_name, region): url = "volumes?name={0}®ion={1}".format(volume_name, region) resp = self.rest.get(url) if resp.status_code != 200: raise DOBlockStorageException(resp.json["message"]) volumes = resp.json["volumes"] if not volumes: return None return volumes[0] def get_attached_droplet_ID(self, volume_name, region): volume = self.get_block_storage_by_name(volume_name, region) if not volume or not volume["droplet_ids"]: return None return volume["droplet_ids"][0] def attach_detach_block_storage(self, method, volume_name, region, droplet_id): data = { "type": method, "volume_name": volume_name, "region": region, "droplet_id": droplet_id, } response = self.rest.post("volumes/actions", data=data) status = response.status_code json = response.json if status == 202: return self.poll_action_for_complete_status(json["action"]["id"]) elif status == 200: return True elif status == 404 and method == "detach": return False # Already detached elif status == 422: return False else: raise DOBlockStorageException(json["message"]) def resize_block_storage(self, volume_name, region, desired_size): if not desired_size: return False volume = self.get_block_storage_by_name(volume_name, region) if volume["size_gigabytes"] == desired_size: return False data = { "type": "resize", "size_gigabytes": desired_size, } resp = self.rest.post( "volumes/{0}/actions".format(volume["id"]), data=data, ) if resp.status_code == 202: return self.poll_action_for_complete_status( resp.json["action"]["id"]) else: # we'd get status 422 if desired_size <= current volume size raise DOBlockStorageException(resp.json["message"]) def create_block_storage(self): volume_name = self.get_key_or_fail("volume_name") snapshot_id = self.module.params["snapshot_id"] if snapshot_id: self.module.params["block_size"] = None self.module.params["region"] = None block_size = None region = None else: block_size = self.get_key_or_fail("block_size") region = self.get_key_or_fail("region") description = self.module.params["description"] data = { "size_gigabytes": block_size, "name": volume_name, "description": description, "region": region, "snapshot_id": snapshot_id, } response = self.rest.post("volumes", data=data) status = response.status_code json = response.json if status == 201: self.module.exit_json(changed=True, id=json["volume"]["id"]) elif status == 409 and json["id"] == "conflict": # The volume exists already, but it might not have the desired size resized = self.resize_block_storage(volume_name, region, block_size) self.module.exit_json(changed=resized) else: raise DOBlockStorageException(json["message"]) def delete_block_storage(self): volume_name = self.get_key_or_fail("volume_name") region = self.get_key_or_fail("region") url = "volumes?name={0}®ion={1}".format(volume_name, region) attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) if attached_droplet_id is not None: self.attach_detach_block_storage("detach", volume_name, region, attached_droplet_id) response = self.rest.delete(url) status = response.status_code json = response.json if status == 204: self.module.exit_json(changed=True) elif status == 404: self.module.exit_json(changed=False) else: raise DOBlockStorageException(json["message"]) def attach_block_storage(self): volume_name = self.get_key_or_fail("volume_name") region = self.get_key_or_fail("region") droplet_id = self.get_key_or_fail("droplet_id") attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) if attached_droplet_id is not None: if attached_droplet_id == droplet_id: self.module.exit_json(changed=False) else: self.attach_detach_block_storage("detach", volume_name, region, attached_droplet_id) changed_status = self.attach_detach_block_storage( "attach", volume_name, region, droplet_id) self.module.exit_json(changed=changed_status) def detach_block_storage(self): volume_name = self.get_key_or_fail("volume_name") region = self.get_key_or_fail("region") droplet_id = self.get_key_or_fail("droplet_id") changed_status = self.attach_detach_block_storage( "detach", volume_name, region, droplet_id) self.module.exit_json(changed=changed_status)
class DOFirewall(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.name = self.module.params.get("name") self.baseurl = "firewalls" self.firewalls = self.get_firewalls() def get_firewalls(self): base_url = self.baseurl + "?" response = self.rest.get("%s" % base_url) status_code = response.status_code status_code_success = 200 if status_code != status_code_success: error = response.json info = response.info if error: error.update({"status_code": status_code}) error.update({"status_code_success": status_code_success}) self.module.fail_json(msg=error) elif info: info.update({"status_code_success": status_code_success}) self.module.fail_json(msg=info) else: msg_error = "Failed to retrieve firewalls from DigitalOcean" self.module.fail_json( msg=msg_error + " (url=" + self.rest.baseurl + "/" + self.baseurl + ", status=" + str(status_code or "") + " - expected:" + str(status_code_success) + ")") return self.rest.get_paginated_data(base_url=base_url, data_key_name="firewalls") def get_firewall_by_name(self): rule = {} for firewall in self.firewalls: if firewall["name"] == self.name: rule.update(firewall) return rule return None def ordered(self, obj): if isinstance(obj, dict): return sorted((k, self.ordered(v)) for k, v in obj.items()) if isinstance(obj, list): return sorted(self.ordered(x) for x in obj) else: return obj def fill_protocol_defaults(self, obj): if obj.get("protocol") is None: obj["protocol"] = "tcp" return obj def fill_source_and_destination_defaults_inner(self, obj): addresses = obj.get("addresses") or [] droplet_ids = obj.get("droplet_ids") or [] droplet_ids = [str(droplet_id) for droplet_id in droplet_ids] load_balancer_uids = obj.get("load_balancer_uids") or [] load_balancer_uids = [str(uid) for uid in load_balancer_uids] tags = obj.get("tags") or [] data = { "addresses": addresses, "droplet_ids": droplet_ids, "load_balancer_uids": load_balancer_uids, "tags": tags, } return data def fill_sources_and_destinations_defaults(self, obj, prop): value = obj.get(prop) if value is None: value = {} else: value = self.fill_source_and_destination_defaults_inner(value) obj[prop] = value return obj def fill_data_defaults(self, obj): inbound_rules = obj.get("inbound_rules") if inbound_rules is None: inbound_rules = [] else: inbound_rules = [ self.fill_protocol_defaults(x) for x in inbound_rules ] inbound_rules = [ self.fill_sources_and_destinations_defaults(x, "sources") for x in inbound_rules ] outbound_rules = obj.get("outbound_rules") if outbound_rules is None: outbound_rules = [] else: outbound_rules = [ self.fill_protocol_defaults(x) for x in outbound_rules ] outbound_rules = [ self.fill_sources_and_destinations_defaults(x, "destinations") for x in outbound_rules ] droplet_ids = obj.get("droplet_ids") or [] droplet_ids = [str(droplet_id) for droplet_id in droplet_ids] tags = obj.get("tags") or [] data = { "name": obj.get("name"), "inbound_rules": inbound_rules, "outbound_rules": outbound_rules, "droplet_ids": droplet_ids, "tags": tags, } return data def data_to_compare(self, obj): return self.ordered(self.fill_data_defaults(obj)) def update(self, obj, id): if id is None: status_code_success = 202 resp = self.rest.post(path=self.baseurl, data=obj) else: status_code_success = 200 resp = self.rest.put(path=self.baseurl + "/" + id, data=obj) status_code = resp.status_code if status_code != status_code_success: error = resp.json error.update({ "context": "error when trying to " + ("create" if (id is None) else "update") + " firewalls" }) error.update({"status_code": status_code}) error.update({"status_code_success": status_code_success}) self.module.fail_json(msg=error) self.module.exit_json(changed=True, data=resp.json["firewall"]) def create(self): rule = self.get_firewall_by_name() data = { "name": self.module.params.get("name"), "inbound_rules": self.module.params.get("inbound_rules"), "outbound_rules": self.module.params.get("outbound_rules"), "droplet_ids": self.module.params.get("droplet_ids"), "tags": self.module.params.get("tags"), } if rule is None: self.update(data, None) else: rule_data = { "name": rule.get("name"), "inbound_rules": rule.get("inbound_rules"), "outbound_rules": rule.get("outbound_rules"), "droplet_ids": rule.get("droplet_ids"), "tags": rule.get("tags"), } user_data = { "name": data.get("name"), "inbound_rules": data.get("inbound_rules"), "outbound_rules": data.get("outbound_rules"), "droplet_ids": data.get("droplet_ids"), "tags": data.get("tags"), } if self.data_to_compare(user_data) == self.data_to_compare( rule_data): self.module.exit_json(changed=False, data=rule) else: self.update(data, rule.get("id")) def destroy(self): rule = self.get_firewall_by_name() if rule is None: self.module.exit_json(changed=False, data="Firewall does not exist") else: endpoint = self.baseurl + "/" + rule["id"] resp = self.rest.delete(path=endpoint) status_code = resp.status_code if status_code != 204: self.module.fail_json(msg="Failed to delete firewall") self.module.exit_json( changed=True, data="Deleted firewall rule: {0} - {1}".format( rule["name"], rule["id"]), )
class DOLoadBalancer(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.id = None self.name = self.module.params.get("name") self.region = self.module.params.get("region") self.updates = [] # Pop these values so we don't include them in the POST data self.module.params.pop("oauth_token") self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 600) def get_by_id(self): """Fetch an existing DigitalOcean Load Balancer (by id) API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/get_load_balancer """ response = self.rest.get("load_balancers/{0}".format(self.id)) json_data = response.json if response.status_code == 200: # Found one with the given id: lb = json_data.get("load_balancer", None) if lb is not None: self.lb = lb return lb else: self.module.fail_json( msg="Unexpected error; please file a bug: get_by_id") return None def get_by_name(self): """Fetch all existing DigitalOcean Load Balancers API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_load_balancers """ page = 1 while page is not None: response = self.rest.get("load_balancers?page={0}".format(page)) json_data = response.json if json_data is None: self.module.fail_json( msg= "Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds." ) if response.status_code == 200: lbs = json_data.get("load_balancers", []) for lb in lbs: # Found one with the same name: name = lb.get("name", None) if name == self.name: # Make sure the region is the same! region = lb.get("region", None) if region is not None: region_slug = region.get("slug", None) if region_slug is not None: if region_slug == self.region: self.lb = lb return lb else: self.module.fail_json( msg= "Cannot change load balancer region -- delete and re-create" ) else: self.module.fail_json( msg= "Unexpected error; please file a bug: get_by_name" ) else: self.module.fail_json( msg= "Unexpected error; please file a bug: get_by_name" ) if ("links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"]): page += 1 else: page = None else: self.module.fail_json( msg="Unexpected error; please file a bug: get_by_name") return None def ensure_active(self): """Wait for the existing Load Balancer to be active""" end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: if self.get_by_id(): status = self.lb.get("status", None) if status is not None: if status == "active": return True else: self.module.fail_json( msg="Unexpected error; please file a bug: ensure_active" ) else: self.module.fail_json( msg="Load Balancer {0} in {1} not found".format( self.id, self.region)) time.sleep(min(10, end_time - time.monotonic())) self.module.fail_json( msg="Timed out waiting for Load Balancer {0} in {1} to be active". format(self.id, self.region)) def is_same(self, found_lb): """Checks if exising Load Balancer is the same as requested""" check_attributes = [ "droplet_ids", "size", "algorithm", "forwarding_rules", "health_check", "sticky_sessions", "redirect_http_to_https", "enable_proxy_protocol", "enable_backend_keepalive", ] for attribute in check_attributes: if self.module.params.get(attribute, None) != found_lb.get( attribute, None): # raise Exception(str(self.module.params.get(attribute, None)), str(found_lb.get(attribute, None))) self.updates.append(attribute) # Check if the VPC needs changing. vpc_uuid = self.lb.get("vpc_uuid", None) if vpc_uuid is not None: if vpc_uuid != found_lb.get("vpc_uuid", None): self.updates.append("vpc_uuid") if len(self.updates): return False else: return True def update(self): """Updates a DigitalOcean Load Balancer API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/update_load_balancer """ request_params = dict(self.module.params) self.id = self.lb.get("id", None) self.name = self.lb.get("name", None) if self.id is not None and self.name is not None: response = self.rest.put("load_balancers/{0}".format(self.id), data=request_params) json_data = response.json if response.status_code == 200: self.module.exit_json( changed=True, msg="Load Balancer {0} ({1}) in {2} updated: {3}".format( self.name, self.id, self.region, ", ".join(self.updates)), ) else: self.module.fail_json( changed=False, msg="Error updating Load Balancer {0} ({1}) in {2}: {3}". format(self.name, self.id, self.region, json_data["message"]), ) else: self.module.fail_json( msg="Unexpected error; please file a bug: update") def create(self): """Creates a DigitalOcean Load Balancer API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/create_load_balancer """ # Check if it exists already (the API docs aren't up-to-date right now, # "name" is required and must be unique across the account. found_lb = self.get_by_name() if found_lb is not None: # Do we need to update it? if not self.is_same(found_lb): self.update() else: self.module.exit_json( changed=False, msg= "Load Balancer {0} already exists in {1} (and needs no changes)" .format(self.name, self.region), ) # Create it. request_params = dict(self.module.params) response = self.rest.post("load_balancers", data=request_params) json_data = response.json if response.status_code != 202: self.module.fail_json( msg="Failed creating Load Balancer {0} in {1}: {2}".format( self.name, self.region, json_data["message"])) # Store it. lb = json_data.get("load_balancer", None) if lb is None: self.module.fail_json( msg="Unexpected error; please file a bug: create empty lb") self.id = lb.get("id", None) if self.id is None: self.module.fail_json( msg="Unexpected error; please file a bug: create missing id") if self.wait: self.ensure_active() self.module.exit_json(changed=True, data=json_data) def delete(self): """Deletes a DigitalOcean Load Balancer API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/delete_load_balancer """ lb = self.get_by_name() if lb is not None: id = lb.get("id", None) name = lb.get("name", None) if id is None or name is None: self.module.fail_json( msg="Unexpected error; please file a bug: delete") else: lb_region = lb.get("region", None) region = lb_region.get("slug", None) if region is None: self.module.fail_json( msg="Unexpected error; please file a bug: delete") response = self.rest.delete("load_balancers/{0}".format(id)) json_data = response.json if response.status_code == 204: # Response body should be empty self.module.exit_json( changed=True, msg="Load Balancer {0} ({1}) in {2} deleted".format( name, id, region), ) else: message = json_data.get( "message", "Empty failure message from the DigitalOcean API!") self.module.fail_json( changed=False, msg= "Failed to delete Load Balancer {0} ({1}) in {2}: {3}". format(name, id, region, message), ) else: self.module.fail_json( changed=False, msg="Load Balancer {0} not found in {1}".format( self.name, self.region), )
class DOVPC(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.name = module.params.get("name", None) self.description = module.params.get("description", None) self.default = module.params.get("default", False) self.region = module.params.get("region", None) self.ip_range = module.params.get("ip_range", None) self.vpc_id = module.params.get("vpc_id", None) def get_by_name(self): page = 1 while page is not None: response = self.rest.get("vpcs?page={0}".format(page)) json_data = response.json if response.status_code == 200: for vpc in json_data["vpcs"]: if vpc.get("name", None) == self.name: return vpc if ("links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"]): page += 1 else: page = None return None def create(self): if self.module.check_mode: return self.module.exit_json(changed=True) vpc = self.get_by_name() if vpc is not None: # update vpc_id = vpc.get("id", None) if vpc_id is not None: data = { "name": self.name, } if self.description is not None: data["description"] = self.description if self.default is not False: data["default"] = True response = self.rest.put("vpcs/{0}".format(vpc_id), data=data) json = response.json if response.status_code != 200: self.module.fail_json( msg="Failed to update VPC {0}: {1}".format( self.name, json["message"])) else: self.module.exit_json(changed=False, data=json) else: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug") else: # create data = { "name": self.name, "region": self.region, } if self.description is not None: data["description"] = self.description if self.ip_range is not None: data["ip_range"] = self.ip_range response = self.rest.post("vpcs", data=data) status = response.status_code json = response.json if status == 201: self.module.exit_json(changed=True, data=json["vpc"]) else: self.module.fail_json( changed=False, msg="Failed to create VPC: {0}".format(json["message"]), ) def delete(self): if self.module.check_mode: return self.module.exit_json(changed=True) vpc = self.get_by_name() if vpc is None: self.module.fail_json( msg="Unable to find VPC {0}".format(self.name)) else: vpc_id = vpc.get("id", None) if vpc_id is not None: response = self.rest.delete("vpcs/{0}".format(str(vpc_id))) status = response.status_code json = response.json if status == 204: self.module.exit_json( changed=True, msg="Deleted VPC {0} ({1})".format(self.name, vpc_id), ) else: json = response.json self.module.fail_json( changed=False, msg="Failed to delete VPC {0} ({1}): {2}".format( self.name, vpc_id, json["message"]), )
class DOBlockStorage(object): def __init__(self, module): self.module = module self.rest = DigitalOceanHelper(module) def get_key_or_fail(self, k): v = self.module.params[k] if v is None: self.module.fail_json(msg='Unable to load %s' % k) return v def poll_action_for_complete_status(self, action_id): url = 'actions/{0}'.format(action_id) end_time = time.time() + self.module.params['timeout'] while time.time() < end_time: time.sleep(2) response = self.rest.get(url) status = response.status_code json = response.json if status == 200: if json['action']['status'] == 'completed': return True elif json['action']['status'] == 'errored': raise DOBlockStorageException(json['message']) raise DOBlockStorageException('Unable to reach api.digitalocean.com') def get_block_storage_by_name(self, volume_name, region): url = 'volumes?name={0}®ion={1}'.format(volume_name, region) resp = self.rest.get(url) if resp.status_code != 200: raise DOBlockStorageException(resp.json['message']) volumes = resp.json['volumes'] if not volumes: return None return volumes[0] def get_attached_droplet_ID(self, volume_name, region): volume = self.get_block_storage_by_name(volume_name, region) if not volume or not volume['droplet_ids']: return None return volume['droplet_ids'][0] def attach_detach_block_storage(self, method, volume_name, region, droplet_id): data = { 'type': method, 'volume_name': volume_name, 'region': region, 'droplet_id': droplet_id } response = self.rest.post('volumes/actions', data=data) status = response.status_code json = response.json if status == 202: return self.poll_action_for_complete_status(json['action']['id']) elif status == 200: return True elif status == 422: return False else: raise DOBlockStorageException(json['message']) def resize_block_storage(self, volume_name, region, desired_size): if not desired_size: return False volume = self.get_block_storage_by_name(volume_name, region) if volume['size_gigabytes'] == desired_size: return False data = { 'type': 'resize', 'size_gigabytes': desired_size, } resp = self.rest.post( 'volumes/{0}/actions'.format(volume['id']), data=data, ) if resp.status_code == 202: return self.poll_action_for_complete_status( resp.json['action']['id']) else: # we'd get status 422 if desired_size <= current volume size raise DOBlockStorageException(resp.json['message']) def create_block_storage(self): volume_name = self.get_key_or_fail('volume_name') snapshot_id = self.module.params['snapshot_id'] if snapshot_id: self.module.params['block_size'] = None self.module.params['region'] = None block_size = None region = None else: block_size = self.get_key_or_fail('block_size') region = self.get_key_or_fail('region') description = self.module.params['description'] data = { 'size_gigabytes': block_size, 'name': volume_name, 'description': description, 'region': region, 'snapshot_id': snapshot_id, } response = self.rest.post("volumes", data=data) status = response.status_code json = response.json if status == 201: self.module.exit_json(changed=True, id=json['volume']['id']) elif status == 409 and json['id'] == 'conflict': # The volume exists already, but it might not have the desired size resized = self.resize_block_storage(volume_name, region, block_size) self.module.exit_json(changed=resized) else: raise DOBlockStorageException(json['message']) def delete_block_storage(self): volume_name = self.get_key_or_fail('volume_name') region = self.get_key_or_fail('region') url = 'volumes?name={0}®ion={1}'.format(volume_name, region) attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) if attached_droplet_id is not None: self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) response = self.rest.delete(url) status = response.status_code json = response.json if status == 204: self.module.exit_json(changed=True) elif status == 404: self.module.exit_json(changed=False) else: raise DOBlockStorageException(json['message']) def attach_block_storage(self): volume_name = self.get_key_or_fail('volume_name') region = self.get_key_or_fail('region') droplet_id = self.get_key_or_fail('droplet_id') attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) if attached_droplet_id is not None: if attached_droplet_id == droplet_id: self.module.exit_json(changed=False) else: self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) changed_status = self.attach_detach_block_storage( 'attach', volume_name, region, droplet_id) self.module.exit_json(changed=changed_status) def detach_block_storage(self): volume_name = self.get_key_or_fail('volume_name') region = self.get_key_or_fail('region') droplet_id = self.get_key_or_fail('droplet_id') changed_status = self.attach_detach_block_storage( 'detach', volume_name, region, droplet_id) self.module.exit_json(changed=changed_status)
class DODroplet(object): failure_message = { "empty_response": "Empty response from the DigitalOcean API; please try again or open a bug if it never " "succeeds.", "resizing_off": "Droplet must be off prior to resizing: " "https://docs.digitalocean.com/reference/api/api-reference/#operation/post_droplet_action", "unexpected": "Unexpected error [{0}]; please file a bug: " "https://github.com/ansible-collections/community.digitalocean/issues", "support_action": "Error status on Droplet action [{0}], please try again or contact DigitalOcean support: " "https://docs.digitalocean.com/support/", "failed_to": "Failed to {0} {1} [HTTP {2}: {3}]", } def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 120) self.unique_name = self.module.params.pop("unique_name", False) # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.id = None self.name = None self.size = None self.status = None if self.module.params.get("project"): # only load for non-default project assignments self.projects = DigitalOceanProjects(module, self.rest) self.firewalls = self.get_firewalls() self.sleep_interval = self.module.params.pop("sleep_interval", 10) if self.wait: if self.sleep_interval > self.wait_timeout: self.module.fail_json( msg="Sleep interval {0} should be less than {1}".format( self.sleep_interval, self.wait_timeout)) if self.sleep_interval <= 0: self.module.fail_json( msg="Sleep interval {0} should be greater than zero". format(self.sleep_interval)) def get_firewalls(self): response = self.rest.get("firewalls") status_code = response.status_code json_data = response.json if status_code != 200: self.module.fail_json(msg="Failed to get firewalls", data=json_data) return self.rest.get_paginated_data(base_url="firewalls?", data_key_name="firewalls") def get_firewall_by_name(self): rule = {} item = 0 for firewall in self.firewalls: for firewall_name in self.module.params["firewall"]: if firewall_name in firewall["name"]: rule[item] = {} rule[item].update(firewall) item += 1 if len(rule) > 0: return rule return None def add_droplet_to_firewalls(self): changed = False rule = self.get_firewall_by_name() if rule is None: err = "Failed to find firewalls: {0}".format( self.module.params["firewall"]) return err json_data = self.get_droplet() if json_data is not None: request_params = {} droplet = json_data.get("droplet", None) droplet_id = droplet.get("id", None) request_params["droplet_ids"] = [droplet_id] for firewall in rule: if droplet_id not in rule[firewall]["droplet_ids"]: response = self.rest.post( "firewalls/{0}/droplets".format(rule[firewall]["id"]), data=request_params, ) json_data = response.json status_code = response.status_code if status_code != 204: err = "Failed to add droplet {0} to firewall {1}".format( droplet_id, rule[firewall]["id"]) return err, changed changed = True return None, changed def remove_droplet_from_firewalls(self): changed = False json_data = self.get_droplet() if json_data is not None: request_params = {} droplet = json_data.get("droplet", None) droplet_id = droplet.get("id", None) request_params["droplet_ids"] = [droplet_id] for firewall in self.firewalls: if (firewall["name"] not in self.module.params["firewall"] and droplet_id in firewall["droplet_ids"]): response = self.rest.delete( "firewalls/{0}/droplets".format(firewall["id"]), data=request_params, ) json_data = response.json status_code = response.status_code if status_code != 204: err = "Failed to remove droplet {0} from firewall {1}".format( droplet_id, firewall["id"]) return err, changed changed = True return None, changed def get_by_id(self, droplet_id): if not droplet_id: return None response = self.rest.get("droplets/{0}".format(droplet_id)) status_code = response.status_code json_data = response.json if json_data is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["empty_response"], ) else: if status_code == 200: droplet = json_data.get("droplet", None) if droplet is not None: self.id = droplet.get("id", None) self.name = droplet.get("name", None) self.size = droplet.get("size_slug", None) self.status = droplet.get("status", None) return json_data return None def get_by_name(self, droplet_name): if not droplet_name: return None page = 1 while page is not None: response = self.rest.get("droplets?page={0}".format(page)) json_data = response.json status_code = response.status_code if json_data is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["empty_response"], ) else: if status_code == 200: droplets = json_data.get("droplets", []) for droplet in droplets: if droplet.get("name", None) == droplet_name: self.id = droplet.get("id", None) self.name = droplet.get("name", None) self.size = droplet.get("size_slug", None) self.status = droplet.get("status", None) return {"droplet": droplet} if ("links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"]): page += 1 else: page = None return None def get_addresses(self, data): """Expose IP addresses as their own property allowing users extend to additional tasks""" _data = data for k, v in data.items(): setattr(self, k, v) networks = _data["droplet"]["networks"] for network in networks.get("v4", []): if network["type"] == "public": _data["ip_address"] = network["ip_address"] else: _data["private_ipv4_address"] = network["ip_address"] for network in networks.get("v6", []): if network["type"] == "public": _data["ipv6_address"] = network["ip_address"] else: _data["private_ipv6_address"] = network["ip_address"] return _data def get_droplet(self): json_data = self.get_by_id(self.module.params["id"]) if not json_data and self.unique_name: json_data = self.get_by_name(self.module.params["name"]) return json_data def resize_droplet(self, state, droplet_id): if self.status != "off": self.module.fail_json( changed=False, msg=DODroplet.failure_message["resizing_off"], ) self.wait_action( droplet_id, { "type": "resize", "disk": self.module.params["resize_disk"], "size": self.module.params["size"], }, ) if state == "active": self.ensure_power_on(droplet_id) # Get updated Droplet data json_data = self.get_droplet() droplet = json_data.get("droplet", None) if droplet is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no Droplet"), ) self.module.exit_json( changed=True, msg="Resized Droplet {0} ({1}) from {2} to {3}".format( self.name, self.id, self.size, self.module.params["size"]), data={"droplet": droplet}, ) def wait_status(self, droplet_id, desired_statuses): # Make sure Droplet is active first end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}".format(droplet_id)) json_data = response.json status_code = response.status_code message = json_data.get("message", "no error message") droplet = json_data.get("droplet", None) droplet_status = droplet.get("status", None) if droplet else None if droplet is None or droplet_status is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no Droplet or status"), ) if status_code >= 400: self.module.fail_json( changed=False, msg=DODroplet.failure_message["failed_to"].format( "get", "Droplet", status_code, message), ) if droplet_status in desired_statuses: return time.sleep(self.sleep_interval) self.module.fail_json(msg="Wait for Droplet [{0}] status timeout". format(",".join(desired_statuses))) def wait_check_action(self, droplet_id, action_id): end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("droplets/{0}/actions/{1}".format( droplet_id, action_id)) json_data = response.json status_code = response.status_code message = json_data.get("message", "no error message") action = json_data.get("action", None) action_id = action.get("id", None) action_status = action.get("status", None) if action is None or action_id is None or action_status is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no action, ID, or status"), ) if status_code >= 400: self.module.fail_json( changed=False, msg=DODroplet.failure_message["failed_to"].format( "get", "action", status_code, message), ) if action_status == "errored": self.module.fail_json( changed=True, msg=DODroplet.failure_message["support_action"].format( action_id), ) if action_status == "completed": return time.sleep(self.sleep_interval) self.module.fail_json(msg="Wait for Droplet action timeout") def wait_action(self, droplet_id, desired_action_data): action_type = desired_action_data.get("type", "undefined") response = self.rest.post("droplets/{0}/actions".format(droplet_id), data=desired_action_data) json_data = response.json status_code = response.status_code message = json_data.get("message", "no error message") action = json_data.get("action", None) action_id = action.get("id", None) action_status = action.get("status", None) if action is None or action_id is None or action_status is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no action, ID, or status"), ) if status_code >= 400: self.module.fail_json( changed=False, msg=DODroplet.failure_message["failed_to"].format( "post", "action", status_code, message), ) # Keep checking till it is done or times out self.wait_check_action(droplet_id, action_id) def ensure_power_on(self, droplet_id): # Make sure Droplet is active or off first self.wait_status(droplet_id, ["active", "off"]) # Trigger power-on self.wait_action(droplet_id, {"type": "power_on"}) def ensure_power_off(self, droplet_id): # Make sure Droplet is active first self.wait_status(droplet_id, ["active"]) # Trigger power-off self.wait_action(droplet_id, {"type": "power_off"}) def create(self, state): json_data = self.get_droplet() # We have the Droplet if json_data is not None: droplet = json_data.get("droplet", None) droplet_id = droplet.get("id", None) droplet_size = droplet.get("size_slug", None) if droplet_id is None or droplet_size is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no Droplet ID or size"), ) # Add droplet to a firewall if specified if self.module.params["firewall"] is not None: firewall_changed = False if len(self.module.params["firewall"]) > 0: firewall_add, add_changed = self.add_droplet_to_firewalls() if firewall_add is not None: self.module.fail_json( changed=False, msg=firewall_add, data={ "droplet": droplet, "firewall": firewall_add }, ) firewall_changed = firewall_changed or add_changed firewall_remove, remove_changed = self.remove_droplet_from_firewalls( ) if firewall_remove is not None: self.module.fail_json( changed=False, msg=firewall_remove, data={ "droplet": droplet, "firewall": firewall_remove }, ) firewall_changed = firewall_changed or remove_changed self.module.exit_json( changed=firewall_changed, data={"droplet": droplet}, ) # Check mode if self.module.check_mode: self.module.exit_json(changed=False) # Ensure Droplet size if droplet_size != self.module.params.get("size", None): self.resize_droplet(state, droplet_id) # Ensure Droplet power state droplet_data = self.get_addresses(json_data) droplet_id = droplet.get("id", None) droplet_status = droplet.get("status", None) if droplet_id is not None and droplet_status is not None: if state == "active" and droplet_status != "active": self.ensure_power_on(droplet_id) # Get updated Droplet data (fallback to current data) json_data = self.get_droplet() droplet = json_data.get("droplet", droplet) self.module.exit_json(changed=True, data={"droplet": droplet}) elif state == "inactive" and droplet_status != "off": self.ensure_power_off(droplet_id) # Get updated Droplet data (fallback to current data) json_data = self.get_droplet() droplet = json_data.get("droplet", droplet) self.module.exit_json(changed=True, data={"droplet": droplet}) else: self.module.exit_json(changed=False, data={"droplet": droplet}) # We don't have the Droplet, create it # Check mode if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) del request_params["id"] response = self.rest.post("droplets", data=request_params) json_data = response.json status_code = response.status_code message = json_data.get("message", "no error message") droplet = json_data.get("droplet", None) # Ensure that the Droplet is created if status_code != 202: self.module.fail_json( changed=False, msg=DODroplet.failure_message["failed_to"].format( "create", "Droplet", status_code, message), ) droplet_id = droplet.get("id", None) if droplet is None or droplet_id is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no Droplet or ID"), ) if status_code >= 400: self.module.fail_json( changed=False, msg=DODroplet.failure_message["failed_to"].format( "create", "Droplet", status_code, message), ) if self.wait: if state == "present" or state == "active": self.ensure_power_on(droplet_id) if state == "inactive": self.ensure_power_off(droplet_id) else: if state == "inactive": self.ensure_power_off(droplet_id) # Get updated Droplet data (fallback to current data) if self.wait: json_data = self.get_by_id(droplet_id) if json_data: droplet = json_data.get("droplet", droplet) project_name = self.module.params.get("project") if project_name: # empty string is the default project, skip project assignment urn = "do:droplet:{0}".format(droplet_id) assign_status, error_message, resources = self.projects.assign_to_project( project_name, urn) self.module.exit_json( changed=True, data={"droplet": droplet}, msg=error_message, assign_status=assign_status, resources=resources, ) # Add droplet to firewall if specified if self.module.params["firewall"] is not None: # raise Exception(self.module.params["firewall"]) firewall_add = self.add_droplet_to_firewalls() if firewall_add is not None: self.module.fail_json( changed=False, msg=firewall_add, data={ "droplet": droplet, "firewall": firewall_add }, ) firewall_remove = self.remove_droplet_from_firewalls() if firewall_remove is not None: self.module.fail_json( changed=False, msg=firewall_remove, data={ "droplet": droplet, "firewall": firewall_remove }, ) self.module.exit_json(changed=True, data={"droplet": droplet}) self.module.exit_json(changed=True, data={"droplet": droplet}) def delete(self): # to delete a droplet we need to know the droplet id or unique name, ie # name is not None and unique_name is True, but as "id or name" is # enforced elsewhere, we only need to enforce "id or unique_name" here if not self.module.params["id"] and not self.unique_name: self.module.fail_json( changed=False, msg="id must be set or unique_name must be true for deletes", ) json_data = self.get_droplet() if json_data is None: self.module.exit_json(changed=False, msg="Droplet not found") # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Delete it droplet = json_data.get("droplet", None) droplet_id = droplet.get("id", None) droplet_name = droplet.get("name", None) if droplet is None or droplet_id is None: self.module.fail_json( changed=False, msg=DODroplet.failure_message["unexpected"].format( "no Droplet, name, or ID"), ) response = self.rest.delete("droplets/{0}".format(droplet_id)) json_data = response.json status_code = response.status_code if status_code == 204: self.module.exit_json( changed=True, msg="Droplet {0} ({1}) deleted".format(droplet_name, droplet_id), ) else: self.module.fail_json( changed=False, msg="Failed to delete Droplet {0} ({1})".format( droplet_name, droplet_id), )
class DOKubernetes(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module # Pop these values so we don't include them in the POST data self.return_kubeconfig = self.module.params.pop( "return_kubeconfig", False) self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 600) self.module.params.pop("oauth_token") self.cluster_id = None def get_by_id(self): """Returns an existing DigitalOcean Kubernetes cluster matching on id""" response = self.rest.get("kubernetes/clusters/{0}".format( self.cluster_id)) json_data = response.json if response.status_code == 200: return json_data return None def get_all_clusters(self): """Returns all DigitalOcean Kubernetes clusters""" response = self.rest.get("kubernetes/clusters") json_data = response.json if response.status_code == 200: return json_data return None def get_by_name(self, cluster_name): """Returns an existing DigitalOcean Kubernetes cluster matching on name""" if not cluster_name: return None clusters = self.get_all_clusters() for cluster in clusters["kubernetes_clusters"]: if cluster["name"] == cluster_name: return cluster return None def get_kubernetes_kubeconfig(self): """Returns the kubeconfig for an existing DigitalOcean Kubernetes cluster""" response = self.rest.get("kubernetes/clusters/{0}/kubeconfig".format( self.cluster_id)) text_data = response.text return text_data def get_kubernetes(self): """Returns an existing DigitalOcean Kubernetes cluster by name""" json_data = self.get_by_name(self.module.params["name"]) if json_data: self.cluster_id = json_data["id"] return json_data else: return None def get_kubernetes_options(self): """Fetches DigitalOcean Kubernetes options: regions, sizes, versions. API reference: https://developers.digitalocean.com/documentation/v2/#list-available-regions--node-sizes--and-versions-of-kubernetes """ response = self.rest.get("kubernetes/options") json_data = response.json if response.status_code == 200: return json_data return None def ensure_running(self): """Waits for the newly created DigitalOcean Kubernetes cluster to be running""" end_time = time.time() + self.wait_timeout while time.time() < end_time: cluster = self.get_by_id() if cluster["kubernetes_cluster"]["status"]["state"] == "running": return cluster time.sleep(min(2, end_time - time.time())) self.module.fail_json(msg="Wait for Kubernetes cluster to be running") def create(self): """Creates a DigitalOcean Kubernetes cluster API reference: https://developers.digitalocean.com/documentation/v2/#create-a-new-kubernetes-cluster """ # Get valid Kubernetes options (regions, sizes, versions) kubernetes_options = self.get_kubernetes_options()["options"] # Validate region valid_regions = [str(x["slug"]) for x in kubernetes_options["regions"]] if self.module.params.get("region") not in valid_regions: self.module.fail_json( msg="Invalid region {0} (valid regions are {1})".format( self.module.params.get("region"), ", ".join( valid_regions))) # Validate version valid_versions = [ str(x["slug"]) for x in kubernetes_options["versions"] ] valid_versions.append("latest") if self.module.params.get("version") not in valid_versions: self.module.fail_json( msg="Invalid version {0} (valid versions are {1})".format( self.module.params.get("version"), ", ".join( valid_versions))) # Validate size valid_sizes = [str(x["slug"]) for x in kubernetes_options["sizes"]] for node_pool in self.module.params.get("node_pools"): if node_pool["size"] not in valid_sizes: self.module.fail_json( msg="Invalid size {0} (valid sizes are {1})".format( node_pool["size"], ", ".join(valid_sizes))) # Create the Kubernetes cluster json_data = self.get_kubernetes() if json_data: # Add the kubeconfig to the return if self.return_kubeconfig: json_data["kubeconfig"] = self.get_kubernetes_kubeconfig() self.module.exit_json(changed=False, data=json_data) if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) response = self.rest.post("kubernetes/clusters", data=request_params) json_data = response.json if response.status_code >= 400: self.module.fail_json(changed=False, msg=json_data) # Set the cluster_id self.cluster_id = json_data["kubernetes_cluster"]["id"] if self.wait: json_data = self.ensure_running() # Add the kubeconfig to the return if self.return_kubeconfig: json_data["kubeconfig"] = self.get_kubernetes_kubeconfig() self.module.exit_json(changed=True, data=json_data) def delete(self): """Deletes a DigitalOcean Kubernetes cluster API reference: https://developers.digitalocean.com/documentation/v2/#delete-a-kubernetes-cluster """ json_data = self.get_kubernetes() if json_data: if self.module.check_mode: self.module.exit_json(changed=True) response = self.rest.delete("kubernetes/clusters/{0}".format( json_data["id"])) json_data = response.json if response.status_code == 204: self.module.exit_json(changed=True, msg="Kubernetes cluster deleted") self.module.fail_json(changed=False, msg="Failed to delete Kubernetes cluster") else: self.module.exit_json(changed=False, msg="Kubernetes cluster not found")
class DODatabase(object): def __init__(self, module): self.module = module self.rest = DigitalOceanHelper(module) if self.module.params.get("project"): # only load for non-default project assignments self.projects = DigitalOceanProjects(module, self.rest) # pop wait and wait_timeout so we don't include it in the POST data self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 600) # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.id = None self.name = None self.engine = None self.version = None self.num_nodes = None self.region = None self.status = None self.size = None def get_by_id(self, database_id): if database_id is None: return None response = self.rest.get("databases/{0}".format(database_id)) json_data = response.json if response.status_code == 200: database = json_data.get("database", None) if database is not None: self.id = database.get("id", None) self.name = database.get("name", None) self.engine = database.get("engine", None) self.version = database.get("version", None) self.num_nodes = database.get("num_nodes", None) self.region = database.get("region", None) self.status = database.get("status", None) self.size = database.get("size", None) return json_data return None def get_by_name(self, database_name): if database_name is None: return None page = 1 while page is not None: response = self.rest.get("databases?page={0}".format(page)) json_data = response.json if response.status_code == 200: databases = json_data.get("databases", None) if databases is None or not isinstance(databases, list): return None for database in databases: if database.get("name", None) == database_name: self.id = database.get("id", None) self.name = database.get("name", None) self.engine = database.get("engine", None) self.version = database.get("version", None) self.status = database.get("status", None) self.num_nodes = database.get("num_nodes", None) self.region = database.get("region", None) self.size = database.get("size", None) return {"database": database} if ( "links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"] ): page += 1 else: page = None return None def get_database(self): json_data = self.get_by_id(self.module.params["id"]) if not json_data: json_data = self.get_by_name(self.module.params["name"]) return json_data def ensure_online(self, database_id): end_time = time.monotonic() + self.wait_timeout while time.monotonic() < end_time: response = self.rest.get("databases/{0}".format(database_id)) json_data = response.json database = json_data.get("database", None) if database is not None: status = database.get("status", None) if status is not None: if status == "online": return json_data time.sleep(10) self.module.fail_json(msg="Waiting for database online timeout") def create(self): json_data = self.get_database() if json_data is not None: database = json_data.get("database", None) if database is not None: self.module.exit_json(changed=False, data=json_data) else: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug" ) if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) del request_params["id"] response = self.rest.post("databases", data=request_params) json_data = response.json if response.status_code >= 400: self.module.fail_json(changed=False, msg=json_data["message"]) database = json_data.get("database", None) if database is None: self.module.fail_json( changed=False, msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues", ) database_id = database.get("id", None) if database_id is None: self.module.fail_json( changed=False, msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues", ) if self.wait: json_data = self.ensure_online(database_id) project_name = self.module.params.get("project") if project_name: # empty string is the default project, skip project assignment urn = "do:dbaas:{0}".format(database_id) assign_status, error_message, resources = self.projects.assign_to_project( project_name, urn ) self.module.exit_json( changed=True, data=json_data, msg=error_message, assign_status=assign_status, resources=resources, ) else: self.module.exit_json(changed=True, data=json_data) def delete(self): json_data = self.get_database() if json_data is not None: if self.module.check_mode: self.module.exit_json(changed=True) database = json_data.get("database", None) database_id = database.get("id", None) database_name = database.get("name", None) database_region = database.get("region", None) if database_id is not None: response = self.rest.delete("databases/{0}".format(database_id)) json_data = response.json if response.status_code == 204: self.module.exit_json( changed=True, msg="Deleted database {0} ({1}) in region {2}".format( database_name, database_id, database_region ), ) self.module.fail_json( changed=False, msg="Failed to delete database {0} ({1}) in region {2}: {3}".format( database_name, database_id, database_region, json_data["message"], ), ) else: self.module.fail_json( changed=False, msg="Unexpected error, please file a bug" ) else: self.module.exit_json( changed=False, msg="Database {0} in region {1} not found".format( self.module.params["name"], self.module.params["region"] ), )
class DOCDNEndpoint(object): def __init__(self, module): self.module = module self.rest = DigitalOceanHelper(module) # pop the oauth token so we don't include it in the POST data self.token = self.module.params.pop("oauth_token") def get_cdn_endpoints(self): cdns = self.rest.get_paginated_data( base_url="cdn/endpoints?", data_key_name="endpoints" ) return cdns def get_cdn_endpoint(self): cdns = self.rest.get_paginated_data( base_url="cdn/endpoints?", data_key_name="endpoints" ) found = None for cdn in cdns: if cdn.get("origin") == self.module.params.get("origin"): found = cdn for key in ["ttl", "certificate_id"]: if self.module.params.get(key) != cdn.get(key): return found, True return found, False def create(self): cdn, needs_update = self.get_cdn_endpoint() if cdn is not None: if not needs_update: # Have it already self.module.exit_json(changed=False, msg=cdn) if needs_update: # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Update it request_params = dict(self.module.params) endpoint = "cdn/endpoints" response = self.rest.put( "{0}/{1}".format(endpoint, cdn.get("id")), data=request_params ) status_code = response.status_code json_data = response.json # The API docs are wrong (they say 202 but return 200) if status_code != 200: self.module.fail_json( changed=False, msg="Failed to put {0} information due to error [HTTP {1}: {2}]".format( endpoint, status_code, json_data.get("message", "(empty error message)"), ), ) self.module.exit_json(changed=True, data=json_data) else: # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Create it request_params = dict(self.module.params) endpoint = "cdn/endpoints" response = self.rest.post(endpoint, data=request_params) status_code = response.status_code json_data = response.json if status_code != 201: self.module.fail_json( changed=False, msg="Failed to post {0} information due to error [HTTP {1}: {2}]".format( endpoint, status_code, json_data.get("message", "(empty error message)"), ), ) self.module.exit_json(changed=True, data=json_data) def delete(self): cdn, needs_update = self.get_cdn_endpoint() if cdn is not None: # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Delete it endpoint = "cdn/endpoints/{0}".format(cdn.get("id")) response = self.rest.delete(endpoint) status_code = response.status_code json_data = response.json if status_code != 204: self.module.fail_json( changed=False, msg="Failed to delete {0} information due to error [HTTP {1}: {2}]".format( endpoint, status_code, json_data.get("message", "(empty error message)"), ), ) self.module.exit_json( changed=True, msg="Deleted CDN Endpoint {0} ({1})".format( cdn.get("origin"), cdn.get("id") ), ) else: self.module.exit_json(changed=False)
class DOSnapshot(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.wait = self.module.params.pop("wait", True) self.wait_timeout = self.module.params.pop("wait_timeout", 120) # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.snapshot_type = module.params["snapshot_type"] self.snapshot_name = module.params["snapshot_name"] self.snapshot_tags = module.params["snapshot_tags"] self.snapshot_id = module.params["snapshot_id"] self.volume_id = module.params["volume_id"] def wait_finished(self): current_time = time.monotonic() end_time = current_time + self.wait_timeout while current_time < end_time: response = self.rest.get("actions/{0}".format(str(self.action_id))) status = response.status_code if status != 200: self.module.fail_json( msg="Unable to find action {0}, please file a bug".format( str(self.action_id))) json = response.json if json["action"]["status"] == "completed": return json time.sleep(10) self.module.fail_json( msg="Timed out waiting for snapshot, action {0}".format( str(self.action_id))) def create(self): if self.module.check_mode: return self.module.exit_json(changed=True) if self.snapshot_type == "droplet": droplet_id = self.module.params["droplet_id"] data = { "type": "snapshot", } if self.snapshot_name is not None: data["name"] = self.snapshot_name response = self.rest.post("droplets/{0}/actions".format( str(droplet_id)), data=data) status = response.status_code json = response.json if status == 201: self.action_id = json["action"]["id"] if self.wait: json = self.wait_finished() self.module.exit_json( changed=True, msg="Created snapshot, action {0}".format( self.action_id), data=json["action"], ) self.module.exit_json( changed=True, msg="Created snapshot, action {0}".format(self.action_id), data=json["action"], ) else: self.module.fail_json( changed=False, msg="Failed to create snapshot: {0}".format( json["message"]), ) elif self.snapshot_type == "volume": data = { "name": self.snapshot_name, "tags": self.snapshot_tags, } response = self.rest.post("volumes/{0}/snapshots".format( str(self.volume_id)), data=data) status = response.status_code json = response.json if status == 201: self.module.exit_json( changed=True, msg="Created snapshot, snapshot {0}".format( json["snapshot"]["id"]), data=json["snapshot"], ) else: self.module.fail_json( changed=False, msg="Failed to create snapshot: {0}".format( json["message"]), ) def delete(self): if self.module.check_mode: return self.module.exit_json(changed=True) response = self.rest.delete("snapshots/{0}".format( str(self.snapshot_id))) status = response.status_code if status == 204: self.module.exit_json( changed=True, msg="Deleted snapshot {0}".format(str(self.snapshot_id)), ) else: json = response.json self.module.fail_json( changed=False, msg="Failed to delete snapshot {0}: {1}".format( self.snapshot_id, json["message"]), )
class DODroplet(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.wait = self.module.params.pop('wait', True) self.wait_timeout = self.module.params.pop('wait_timeout', 120) self.unique_name = self.module.params.pop('unique_name', False) # pop the oauth token so we don't include it in the POST data self.module.params.pop('oauth_token') def get_by_id(self, droplet_id): if not droplet_id: return None response = self.rest.get('droplets/{0}'.format(droplet_id)) json_data = response.json if response.status_code == 200: return json_data return None def get_by_name(self, droplet_name): if not droplet_name: return None page = 1 while page is not None: response = self.rest.get('droplets?page={0}'.format(page)) json_data = response.json if response.status_code == 200: for droplet in json_data['droplets']: if droplet['name'] == droplet_name: return {'droplet': droplet} if 'links' in json_data and 'pages' in json_data['links'] and 'next' in json_data['links']['pages']: page += 1 else: page = None return None def get_addresses(self, data): """ Expose IP addresses as their own property allowing users extend to additional tasks """ _data = data for k, v in data.items(): setattr(self, k, v) networks = _data['droplet']['networks'] for network in networks.get('v4', []): if network['type'] == 'public': _data['ip_address'] = network['ip_address'] else: _data['private_ipv4_address'] = network['ip_address'] for network in networks.get('v6', []): if network['type'] == 'public': _data['ipv6_address'] = network['ip_address'] else: _data['private_ipv6_address'] = network['ip_address'] return _data def get_droplet(self): json_data = self.get_by_id(self.module.params['id']) if not json_data and self.unique_name: json_data = self.get_by_name(self.module.params['name']) return json_data def create(self): json_data = self.get_droplet() droplet_data = None if json_data: droplet_data = self.get_addresses(json_data) self.module.exit_json(changed=False, data=droplet_data) if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) del request_params['id'] response = self.rest.post('droplets', data=request_params) json_data = response.json if response.status_code >= 400: self.module.fail_json(changed=False, msg=json_data['message']) if self.wait: json_data = self.ensure_power_on(json_data['droplet']['id']) droplet_data = self.get_addresses(json_data) self.module.exit_json(changed=True, data=droplet_data) def delete(self): json_data = self.get_droplet() if json_data: if self.module.check_mode: self.module.exit_json(changed=True) response = self.rest.delete('droplets/{0}'.format(json_data['droplet']['id'])) json_data = response.json if response.status_code == 204: self.module.exit_json(changed=True, msg='Droplet deleted') self.module.fail_json(changed=False, msg='Failed to delete droplet') else: self.module.exit_json(changed=False, msg='Droplet not found') def ensure_power_on(self, droplet_id): end_time = time.time() + self.wait_timeout while time.time() < end_time: response = self.rest.get('droplets/{0}'.format(droplet_id)) json_data = response.json if json_data['droplet']['status'] == 'active': return json_data time.sleep(min(2, end_time - time.time())) self.module.fail_json(msg='Wait for droplet powering on timeout')
class DODroplet(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module self.wait = self.module.params.pop('wait', True) self.wait_timeout = self.module.params.pop('wait_timeout', 120) self.unique_name = self.module.params.pop('unique_name', False) # pop the oauth token so we don't include it in the POST data self.module.params.pop('oauth_token') self.id = None self.name = None self.size = None self.status = None def get_by_id(self, droplet_id): if not droplet_id: return None response = self.rest.get('droplets/{0}'.format(droplet_id)) json_data = response.json if response.status_code == 200: droplet = json_data.get('droplet', None) if droplet is not None: self.id = droplet.get('id', None) self.name = droplet.get('name', None) self.size = droplet.get('size_slug', None) self.status = droplet.get('status', None) return json_data return None def get_by_name(self, droplet_name): if not droplet_name: return None page = 1 while page is not None: response = self.rest.get('droplets?page={0}'.format(page)) json_data = response.json if response.status_code == 200: for droplet in json_data['droplets']: if droplet.get('name', None) == droplet_name: self.id = droplet.get('id', None) self.name = droplet.get('name', None) self.size = droplet.get('size_slug', None) self.status = droplet.get('status', None) return {'droplet': droplet} if 'links' in json_data and 'pages' in json_data[ 'links'] and 'next' in json_data['links']['pages']: page += 1 else: page = None return None def get_addresses(self, data): """Expose IP addresses as their own property allowing users extend to additional tasks""" _data = data for k, v in data.items(): setattr(self, k, v) networks = _data['droplet']['networks'] for network in networks.get('v4', []): if network['type'] == 'public': _data['ip_address'] = network['ip_address'] else: _data['private_ipv4_address'] = network['ip_address'] for network in networks.get('v6', []): if network['type'] == 'public': _data['ipv6_address'] = network['ip_address'] else: _data['private_ipv6_address'] = network['ip_address'] return _data def get_droplet(self): json_data = self.get_by_id(self.module.params['id']) if not json_data and self.unique_name: json_data = self.get_by_name(self.module.params['name']) return json_data def resize_droplet(self): """API reference: https://developers.digitalocean.com/documentation/v2/#resize-a-droplet (Must be powered off)""" if self.status == 'off': response = self.rest.post('droplets/{0}/actions'.format(self.id), data={ 'type': 'resize', 'disk': self.module.params['resize_disk'], 'size': self.module.params['size'] }) json_data = response.json if response.status_code == 201: self.module.exit_json( changed=True, msg='Resized Droplet {0} ({1}) from {2} to {3}'.format( self.name, self.id, self.size, self.module.params['size'])) else: self.module.fail_json( msg="Resizing Droplet {0} ({1}) failed [HTTP {2}: {3}]". format(self.name, self.id, response.status_code, response.json.get('message', None))) else: self.module.fail_json( msg= 'Droplet must be off prior to resizing (https://developers.digitalocean.com/documentation/v2/#resize-a-droplet)' ) def create(self, state): json_data = self.get_droplet() droplet_data = None if json_data: droplet = json_data.get('droplet', None) if droplet is not None: droplet_size = droplet.get('size_slug', None) if droplet_size is not None: if droplet_size != self.module.params['size']: self.resize_droplet() droplet_data = self.get_addresses(json_data) # If state is active or inactive, ensure requested and desired power states match droplet = json_data.get('droplet', None) if droplet is not None: droplet_id = droplet.get('id', None) droplet_status = droplet.get('status', None) if droplet_id is not None and droplet_status is not None: if state == 'active' and droplet_status != 'active': power_on_json_data = self.ensure_power_on(droplet_id) self.module.exit_json( changed=True, data=self.get_addresses(power_on_json_data)) elif state == 'inactive' and droplet_status != 'off': power_off_json_data = self.ensure_power_off(droplet_id) self.module.exit_json( changed=True, data=self.get_addresses(power_off_json_data)) else: self.module.exit_json(changed=False, data=droplet_data) if self.module.check_mode: self.module.exit_json(changed=True) request_params = dict(self.module.params) del request_params['id'] response = self.rest.post('droplets', data=request_params) json_data = response.json if response.status_code >= 400: self.module.fail_json(changed=False, msg=json_data['message']) if self.wait: json_data = self.ensure_power_on(json_data['droplet']['id']) droplet_data = self.get_addresses(json_data) self.module.exit_json(changed=True, data=droplet_data) def delete(self): json_data = self.get_droplet() if json_data: if self.module.check_mode: self.module.exit_json(changed=True) response = self.rest.delete('droplets/{0}'.format( json_data['droplet']['id'])) json_data = response.json if response.status_code == 204: self.module.exit_json(changed=True, msg='Droplet deleted') self.module.fail_json(changed=False, msg='Failed to delete droplet') else: self.module.exit_json(changed=False, msg='Droplet not found') def ensure_power_on(self, droplet_id): response = self.rest.post('droplets/{0}/actions'.format(droplet_id), data={'type': 'power_on'}) end_time = time.time() + self.wait_timeout while time.time() < end_time: response = self.rest.get('droplets/{0}'.format(droplet_id)) json_data = response.json if json_data['droplet']['status'] == 'active': return json_data time.sleep(min(2, end_time - time.time())) self.module.fail_json(msg='Wait for droplet powering on timeout') def ensure_power_off(self, droplet_id): response = self.rest.post('droplets/{0}/actions'.format(droplet_id), data={'type': 'power_off'}) end_time = time.time() + self.wait_timeout while time.time() < end_time: response = self.rest.get('droplets/{0}'.format(droplet_id)) json_data = response.json if json_data['droplet']['status'] == 'off': return json_data time.sleep(min(2, end_time - time.time())) self.module.fail_json(msg='Wait for droplet powering off timeout')
class DOProject(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module # pop the oauth token so we don't include it in the POST data self.module.params.pop("oauth_token") self.id = None self.name = None self.purpose = None self.description = None self.environment = None self.is_default = None def get_by_id(self, project_id): if not project_id: return None response = self.rest.get("projects/{0}".format(project_id)) json_data = response.json if response.status_code == 200: project = json_data.get("project", None) if project is not None: self.id = project.get("id", None) self.name = project.get("name", None) self.purpose = project.get("purpose", None) self.description = project.get("description", None) self.environment = project.get("environment", None) self.is_default = project.get("is_default", None) return json_data return None def get_by_name(self, project_name): if not project_name: return None page = 1 while page is not None: response = self.rest.get("projects?page={0}".format(page)) json_data = response.json if response.status_code == 200: for project in json_data["projects"]: if project.get("name", None) == project_name: self.id = project.get("id", None) self.name = project.get("name", None) self.description = project.get("description", None) self.purpose = project.get("purpose", None) self.environment = project.get("environment", None) self.is_default = project.get("is_default", None) return {"project": project} if ( "links" in json_data and "pages" in json_data["links"] and "next" in json_data["links"]["pages"] ): page += 1 else: page = None return None def get_project(self): json_data = self.get_by_id(self.module.params["id"]) if not json_data: json_data = self.get_by_name(self.module.params["name"]) return json_data def create(self, state): json_data = self.get_project() request_params = dict(self.module.params) if json_data is not None: changed = False valid_purpose = [ "Just trying out DigitalOcean", "Class project/Educational Purposes", "Website or blog", "Web Application", "Service or API", "Mobile Application", "Machine Learning/AI/Data Processing", "IoT", "Operational/Developer tooling", ] for key in request_params.keys(): if ( key == "purpose" and request_params[key] is not None and request_params[key] not in valid_purpose ): param = "Other: " + request_params[key] else: param = request_params[key] if json_data["project"][key] != param and param is not None: changed = True if changed: response = self.rest.put( "projects/{0}".format(json_data["project"]["id"]), data=request_params, ) if response.status_code != 200: self.module.fail_json(changed=False, msg="Unable to update project") self.module.exit_json(changed=True, data=response.json) else: self.module.exit_json(changed=False, data=json_data) else: response = self.rest.post("projects", data=request_params) if response.status_code != 201: self.module.fail_json(changed=False, msg="Unable to create project") self.module.exit_json(changed=True, data=response.json) def delete(self): json_data = self.get_project() if json_data: if self.module.check_mode: self.module.exit_json(changed=True) response = self.rest.delete( "projects/{0}".format(json_data["project"]["id"]) ) json_data = response.json if response.status_code == 204: self.module.exit_json(changed=True, msg="Project deleted") self.module.fail_json(changed=False, msg="Failed to delete project") else: self.module.exit_json(changed=False, msg="Project not found")
class DOMonitoringAlerts(object): def __init__(self, module): self.rest = DigitalOceanHelper(module) self.module = module # Pop these values so we don't include them in the POST data self.module.params.pop("oauth_token") def get_alerts(self): alerts = self.rest.get_paginated_data(base_url="monitoring/alerts?", data_key_name="policies") return alerts def get_alert(self): alerts = self.rest.get_paginated_data(base_url="monitoring/alerts?", data_key_name="policies") for alert in alerts: for alert_key in alert_keys: if alert.get(alert_key, None) != self.module.params.get( alert_key, None): break # This key doesn't match, try the next alert. else: return alert # Didn't hit break, this alert matches. return None def create(self): # Check for an existing (same) one. alert = self.get_alert() if alert is not None: self.module.exit_json( changed=False, data=alert, ) # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Create it. request_params = dict(self.module.params) response = self.rest.post("monitoring/alerts", data=request_params) if response.status_code == 200: alert = self.get_alert() if alert is not None: self.module.exit_json( changed=True, data=alert, ) else: self.module.fail_json( changed=False, msg="Unexpected error; please file a bug: create") else: self.module.fail_json( msg="Create Monitoring Alert '{0}' failed [HTTP {1}: {2}]". format( self.module.params.get("description"), response.status_code, response.json.get("message", None), )) def delete(self): uuid = self.module.params.get("uuid", None) if uuid is not None: # Check mode if self.module.check_mode: self.module.exit_json(changed=True) # Delete it response = self.rest.delete("monitoring/alerts/{0}".format(uuid)) if response.status_code == 204: self.module.exit_json( changed=True, msg="Deleted Monitoring Alert {0}".format(uuid), ) else: self.module.fail_json( msg="Delete Monitoring Alert {0} failed [HTTP {1}: {2}]". format( uuid, response.status_code, response.json.get("message", None), )) else: self.module.fail_json( changed=False, msg="Unexpected error; please file a bug: delete")