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']))
Example #3
0
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)
Example #4
0
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}&region={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}&region={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"]),
            )
Example #7
0
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),
            )
Example #8
0
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"]),
                    )
Example #9
0
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}&region={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}&region={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)
Example #10
0
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"]
                ),
            )
Example #13
0
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)
Example #14
0
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')
Example #16
0
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')
Example #17
0
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")