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 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']))
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 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 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"]), )