Ejemplo n.º 1
0
 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 __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)
     if self.module.params.get("project"):
         # only load for non-default project assignments
         self.projects = DigitalOceanProjects(module, self.rest)
 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 create_floating_ips(module, rest):
    payload = {}

    if module.params["region"] is not None:
        payload["region"] = module.params["region"]
    if module.params["droplet_id"] is not None:
        payload["droplet_id"] = module.params["droplet_id"]

    # Get existing floating IPs
    response = rest.get("floating_ips/")
    status_code = response.status_code
    json_data = response.json

    # Exit unchanged if any of them are assigned to this Droplet already
    if status_code == 200:
        floating_ips = json_data.get("floating_ips", [])
        if len(floating_ips) != 0:
            for floating_ip in floating_ips:
                droplet = floating_ip.get("droplet", None)
                if droplet is not None:
                    droplet_id = droplet.get("id", None)
                    if droplet_id is not None:
                        if str(droplet_id) == module.params["droplet_id"]:
                            ip = floating_ip.get("ip", None)
                            if ip is not None:
                                module.exit_json(
                                    changed=False, data={"floating_ip": floating_ip}
                                )
                            else:
                                module.fail_json(
                                    changed=False,
                                    msg="Unexpected error querying floating ip",
                                )

    response = rest.post("floating_ips", data=payload)
    status_code = response.status_code
    json_data = response.json
    if status_code == 202:
        if module.params.get(
            "project"
        ):  # only load for non-default project assignments
            rest = DigitalOceanHelper(module)
            projects = DigitalOceanProjects(module, rest)
            project_name = module.params.get("project")
            if (
                project_name
            ):  # empty string is the default project, skip project assignment
                floating_ip = json_data.get("floating_ip")
                ip = floating_ip.get("ip")
                if ip:
                    urn = "do:floatingip:{0}".format(ip)
                    (
                        assign_status,
                        error_message,
                        resources,
                    ) = projects.assign_to_project(project_name, urn)
                    module.exit_json(
                        changed=True,
                        data=json_data,
                        msg=error_message,
                        assign_status=assign_status,
                        resources=resources,
                    )
                else:
                    module.exit_json(
                        changed=True,
                        msg="Floating IP created but not assigned to the {0} Project (missing information from the API response)".format(
                            project_name
                        ),
                        data=json_data,
                    )
            else:
                module.exit_json(changed=True, data=json_data)
        else:
            module.exit_json(changed=True, data=json_data)
    else:
        module.fail_json(
            msg="Error creating floating ip [{0}: {1}]".format(
                status_code, json_data["message"]
            ),
            region=module.params["region"],
        )
 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)
class DOBlockStorage(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)

    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.monotonic() + self.module.params["timeout"]
        while time.monotonic() < end_time:
            time.sleep(10)
            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 the DigitalOcean API at %s" %
            self.module.params.get("baseurl"))

    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:
            project_name = self.module.params.get("project")
            if (
                    project_name
            ):  # empty string is the default project, skip project assignment
                urn = "do:volume:{0}".format(json["volume"]["id"])
                (
                    assign_status,
                    error_message,
                    resources,
                ) = self.projects.assign_to_project(project_name, urn)
                self.module.exit_json(
                    changed=True,
                    id=json["volume"]["id"],
                    msg=error_message,
                    assign_status=assign_status,
                    resources=resources,
                )
            else:
                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)
Ejemplo n.º 7
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),
            )
def run(module):
    do_manager = DoManager(module)
    state = module.params.get("state")

    if module.params.get("project"):
        # only load for non-default project assignments
        projects = DigitalOceanProjects(module, do_manager)

    domain = do_manager.find()
    if state == "present":
        if not domain:
            domain = do_manager.add()
            if "message" in domain:
                module.fail_json(changed=False, msg=domain["message"])
            else:
                # We're at the mercy of a backend process which we have no visibility into:
                # https://docs.digitalocean.com/reference/api/api-reference/#operation/create_domain
                #
                # In particular: "Keep in mind that, upon creation, the zone_file field will
                # have a value of null until a zone file is generated and propagated through
                # an automatic process on the DigitalOcean servers."
                #
                # Arguably, it's nice to see the records versus null, so, we'll just try a
                # few times before giving up and returning null.

                domain_name = module.params.get("name")
                project_name = module.params.get("project")
                urn = "do:domain:{0}".format(domain_name)

                for i in range(ZONE_FILE_ATTEMPTS):
                    record = do_manager.domain_record()
                    if record is not None and "domain" in record:
                        domain = record.get("domain", None)
                        if domain is not None and "zone_file" in domain:
                            if (
                                    project_name
                            ):  # empty string is the default project, skip project assignment
                                (
                                    assign_status,
                                    error_message,
                                    resources,
                                ) = projects.assign_to_project(
                                    project_name, urn)
                                module.exit_json(
                                    changed=True,
                                    domain=domain,
                                    msg=error_message,
                                    assign_status=assign_status,
                                    resources=resources,
                                )
                            else:
                                module.exit_json(changed=True, domain=domain)
                    time.sleep(ZONE_FILE_SLEEP)
                if (
                        project_name
                ):  # empty string is the default project, skip project assignment
                    (
                        assign_status,
                        error_message,
                        resources,
                    ) = projects.assign_to_project(project_name, urn)
                    module.exit_json(
                        changed=True,
                        domain=domain,
                        msg=error_message,
                        assign_status=assign_status,
                        resources=resources,
                    )
                else:
                    module.exit_json(changed=True, domain=domain)
        else:
            records = do_manager.all_domain_records()
            if module.params.get("ip"):
                at_record = None
                for record in records["domain_records"]:
                    if record["name"] == "@" and record["type"] == "A":
                        at_record = record

                if not at_record:
                    do_manager.create_domain_record()
                    module.exit_json(changed=True, domain=do_manager.find())
                elif not at_record["data"] == module.params.get("ip"):
                    do_manager.edit_domain_record(at_record)
                    module.exit_json(changed=True, domain=do_manager.find())

            if module.params.get("ip6"):
                at_record = None
                for record in records["domain_records"]:
                    if record["name"] == "@" and record["type"] == "AAAA":
                        at_record = record

                if not at_record:
                    do_manager.create_domain_record()
                    module.exit_json(changed=True, domain=do_manager.find())
                elif not at_record["data"] == module.params.get("ip6"):
                    do_manager.edit_domain_record(at_record)
                    module.exit_json(changed=True, domain=do_manager.find())

            module.exit_json(changed=False, domain=do_manager.domain_record())

    elif state == "absent":
        if not domain:
            module.exit_json(changed=False, msg="Domain not found")
        else:
            delete_event = do_manager.destroy_domain()
            if not delete_event:
                module.fail_json(changed=False, msg=delete_event["message"])
            else:
                module.exit_json(changed=True, event=None)
        delete_event = do_manager.destroy_domain()
        module.exit_json(changed=delete_event)
class DODatabase(object):
    def __init__(self, module):
        self.module = module
        self.rest = DigitalOceanHelper(module)
        if self.module.params.get("project"):
            # only load for non-default project assignments
            self.projects = DigitalOceanProjects(module, self.rest)
        # pop wait and wait_timeout so we don't include it in the POST data
        self.wait = self.module.params.pop("wait", True)
        self.wait_timeout = self.module.params.pop("wait_timeout", 600)
        # pop the oauth token so we don't include it in the POST data
        self.module.params.pop("oauth_token")
        self.id = None
        self.name = None
        self.engine = None
        self.version = None
        self.num_nodes = None
        self.region = None
        self.status = None
        self.size = None

    def get_by_id(self, database_id):
        if database_id is None:
            return None
        response = self.rest.get("databases/{0}".format(database_id))
        json_data = response.json
        if response.status_code == 200:
            database = json_data.get("database", None)
            if database is not None:
                self.id = database.get("id", None)
                self.name = database.get("name", None)
                self.engine = database.get("engine", None)
                self.version = database.get("version", None)
                self.num_nodes = database.get("num_nodes", None)
                self.region = database.get("region", None)
                self.status = database.get("status", None)
                self.size = database.get("size", None)
            return json_data
        return None

    def get_by_name(self, database_name):
        if database_name is None:
            return None
        page = 1
        while page is not None:
            response = self.rest.get("databases?page={0}".format(page))
            json_data = response.json
            if response.status_code == 200:
                databases = json_data.get("databases", None)
                if databases is None or not isinstance(databases, list):
                    return None
                for database in databases:
                    if database.get("name", None) == database_name:
                        self.id = database.get("id", None)
                        self.name = database.get("name", None)
                        self.engine = database.get("engine", None)
                        self.version = database.get("version", None)
                        self.status = database.get("status", None)
                        self.num_nodes = database.get("num_nodes", None)
                        self.region = database.get("region", None)
                        self.size = database.get("size", None)
                        return {"database": database}
                if (
                    "links" in json_data
                    and "pages" in json_data["links"]
                    and "next" in json_data["links"]["pages"]
                ):
                    page += 1
                else:
                    page = None
        return None

    def get_database(self):
        json_data = self.get_by_id(self.module.params["id"])
        if not json_data:
            json_data = self.get_by_name(self.module.params["name"])
        return json_data

    def ensure_online(self, database_id):
        end_time = time.monotonic() + self.wait_timeout
        while time.monotonic() < end_time:
            response = self.rest.get("databases/{0}".format(database_id))
            json_data = response.json
            database = json_data.get("database", None)
            if database is not None:
                status = database.get("status", None)
                if status is not None:
                    if status == "online":
                        return json_data
            time.sleep(10)
        self.module.fail_json(msg="Waiting for database online timeout")

    def create(self):
        json_data = self.get_database()

        if json_data is not None:
            database = json_data.get("database", None)
            if database is not None:
                self.module.exit_json(changed=False, data=json_data)
            else:
                self.module.fail_json(
                    changed=False, msg="Unexpected error, please file a bug"
                )

        if self.module.check_mode:
            self.module.exit_json(changed=True)

        request_params = dict(self.module.params)
        del request_params["id"]

        response = self.rest.post("databases", data=request_params)
        json_data = response.json
        if response.status_code >= 400:
            self.module.fail_json(changed=False, msg=json_data["message"])
        database = json_data.get("database", None)
        if database is None:
            self.module.fail_json(
                changed=False,
                msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues",
            )

        database_id = database.get("id", None)
        if database_id is None:
            self.module.fail_json(
                changed=False,
                msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues",
            )

        if self.wait:
            json_data = self.ensure_online(database_id)

        project_name = self.module.params.get("project")
        if project_name:  # empty string is the default project, skip project assignment
            urn = "do:dbaas:{0}".format(database_id)
            assign_status, error_message, resources = self.projects.assign_to_project(
                project_name, urn
            )
            self.module.exit_json(
                changed=True,
                data=json_data,
                msg=error_message,
                assign_status=assign_status,
                resources=resources,
            )
        else:
            self.module.exit_json(changed=True, data=json_data)

    def delete(self):
        json_data = self.get_database()
        if json_data is not None:
            if self.module.check_mode:
                self.module.exit_json(changed=True)
            database = json_data.get("database", None)
            database_id = database.get("id", None)
            database_name = database.get("name", None)
            database_region = database.get("region", None)
            if database_id is not None:
                response = self.rest.delete("databases/{0}".format(database_id))
                json_data = response.json
                if response.status_code == 204:
                    self.module.exit_json(
                        changed=True,
                        msg="Deleted database {0} ({1}) in region {2}".format(
                            database_name, database_id, database_region
                        ),
                    )
                self.module.fail_json(
                    changed=False,
                    msg="Failed to delete database {0} ({1}) in region {2}: {3}".format(
                        database_name,
                        database_id,
                        database_region,
                        json_data["message"],
                    ),
                )
            else:
                self.module.fail_json(
                    changed=False, msg="Unexpected error, please file a bug"
                )
        else:
            self.module.exit_json(
                changed=False,
                msg="Database {0} in region {1} not found".format(
                    self.module.params["name"], self.module.params["region"]
                ),
            )
class 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)
        if self.module.params.get("project"):
            # only load for non-default project assignments
            self.projects = DigitalOceanProjects(module, self.rest)

    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(10)
        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()

        project_name = self.module.params.get("project")
        if project_name:  # empty string is the default project, skip project assignment
            urn = "do:loadbalancer:{0}".format(self.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):
        """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),
            )