From 4427ea88872c9099f7f7b18fc312879561b5d333 Mon Sep 17 00:00:00 2001 From: apavanello Date: Sat, 8 May 2021 05:26:12 -0300 Subject: [PATCH] feat: add dokku_resource_limit and dokku_resource_reserve modules (#104) The `dokku_resource_limit` and `dokku_resource_reserve` modules allow limiting and reserving resources (cpu, ram, ...) for individual apps. Co-authored-by: Leopold Talirz --- README.md | 94 +++++++++++ library/dokku_resource_limit.py | 253 ++++++++++++++++++++++++++++++ library/dokku_resource_reserve.py | 252 +++++++++++++++++++++++++++++ 3 files changed, 599 insertions(+) create mode 100644 library/dokku_resource_limit.py create mode 100644 library/dokku_resource_reserve.py diff --git a/README.md b/README.md index 0047256..cf12213 100644 --- a/README.md +++ b/README.md @@ -603,6 +603,100 @@ Manage the registry configuration for a given dokku application state: absent ``` +### dokku_resource_limit + +Manage resource limits for a given dokku application + +#### Parameters + +|Parameter|Choices/Defaults|Comments| +|---------|----------------|--------| +|app
*required*||The name of the app| +|clear_before|*Choices:* |Clear all resource limits before applying| +|process_type||The process type selector| +|resources||The Resource type and quantity (required when state=present)| +|state|*Choices:* |The state of the resource limits| + +#### Example + +```yaml +- name: Limit CPU and memory of a dokku app + dokku_resource_limit: + app: hello-world + resources: + cpu: 100 + memory: 100 + +- name: name: Limit resources per process type of a dokku app + dokku_resource_limit: + app: hello-world + process_type: web + resources: + cpu: 100 + memory: 100 + +- name: Clear limits before applying new limits + dokku_resource_limit: + app: hello-world + state: present + clear_before: True + resources: + cpu: 100 + memory: 100 + +- name: Remove all resource limits + dokku_resource_limit: + app: hello-world + state: absent +``` + +### dokku_resource_reserve + +Manage resource reservations for a given dokku application + +#### Parameters + +|Parameter|Choices/Defaults|Comments| +|---------|----------------|--------| +|app
*required*||The name of the app| +|clear_before|*Choices:* |Clear all reserves before apply| +|process_type||The process type selector| +|resources||The Resource type and quantity (required when state=present)| +|state|*Choices:* |The state of the resource reservations| + +#### Example + +```yaml +- name: Reserve CPU and memory for a dokku app + dokku_resource_reserve: + app: hello-world + resources: + cpu: 100 + memory: 100 + +- name: Create a reservation per process type of a dokku app + dokku_resource_reserve: + app: hello-world + process_type: web + resources: + cpu: 100 + memory: 100 + +- name: Clear all reservations before applying + dokku_resource_reserve: + app: hello-world + state: present + clear_before: True + resources: + cpu: 100 + memory: 100 + +- name: Remove all resource reservations + dokku_resource_reserve: + app: hello-world + state: absent +``` + ### dokku_service_create Creates a given service diff --git a/library/dokku_resource_limit.py b/library/dokku_resource_limit.py new file mode 100644 index 0000000..a3a4c00 --- /dev/null +++ b/library/dokku_resource_limit.py @@ -0,0 +1,253 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# from ansible.module_utils.basic import * +from ansible.module_utils.basic import AnsibleModule +import subprocess +import re + +DOCUMENTATION = """ +--- +module: dokku_resource_limit +short_description: Manage resource limits for a given dokku application +options: + app: + description: + - The name of the app + required: True + default: null + aliases: [] + resources: + description: + - The Resource type and quantity (required when state=present) + required: False + default: null + aliases: [] + process_type: + description: + - The process type selector + required: False + default: null + alias: [] + clear_before: + description: + - Clear all resource limits before applying + required: False + default: "False" + choices: [ "True", "False" ] + aliases: [] + state: + description: + - The state of the resource limits + required: False + default: present + choices: [ "present", "absent" ] + aliases: [] +author: Alexandre Pavanello e Silva +requirements: [ ] + +""" + +EXAMPLES = """ +- name: Limit CPU and memory of a dokku app + dokku_resource_limit: + app: hello-world + resources: + cpu: 100 + memory: 100 + +- name: name: Limit resources per process type of a dokku app + dokku_resource_limit: + app: hello-world + process_type: web + resources: + cpu: 100 + memory: 100 + +- name: Clear limits before applying new limits + dokku_resource_limit: + app: hello-world + state: present + clear_before: True + resources: + cpu: 100 + memory: 100 + +- name: Remove all resource limits + dokku_resource_limit: + app: hello-world + state: absent +""" + + +def force_list(var): + if isinstance(var, list): + return var + return list(var) + + +def subprocess_check_output(command, split="\n"): + error = None + output = [] + try: + output = subprocess.check_output(command, shell=True) + if isinstance(output, bytes): + output = output.decode("utf-8") + output = str(output).rstrip("\n") + if split is None: + return output, error + + output = output.split(split) + output = force_list(filter(None, output)) + output = [o.strip() for o in output] + except subprocess.CalledProcessError as e: + error = str(e) + return output, error + + +def dokku_resource_clear(data): + error = None + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + command = "dokku resource:limit-clear {0} {1}".format(process_type, data["app"]) + try: + subprocess.check_call(command, shell=True) + except subprocess.CalledProcessError as e: + error = str(e) + return error + + +def dokku_resource_limit_report(data): + + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + command = "dokku --quiet resource:limit {0} {1}".format(process_type, data["app"]) + + output, error = subprocess_check_output(command) + if error is not None: + return output, error + output = [re.sub(r"\s+", "", line) for line in output] + + report = {} + + for line in output: + if ":" not in line: + continue + key, value = line.split(":", 1) + report[key] = value + + return report, error + + +def dokku_resource_limit_present(data): + is_error = True + has_changed = False + meta = {"present": False} + + if "resources" not in data: + meta["error"] = "missing required arguments: resources" + return (is_error, has_changed, meta) + + report, error = dokku_resource_limit_report(data) + meta["debug"] = report.keys() + if error: + meta["error"] = error + return (is_error, has_changed, meta) + + for k, v in data["resources"].items(): + if k not in report.keys(): + is_error = True + has_changed = False + meta["error"] = "Unknown resource {0}, choose one of: {1}".format( + k, list(report.keys()) + ) + return (is_error, has_changed, meta) + if report[k] != str(v): + has_changed = True + + if data["clear_before"] is True: + + error = dokku_resource_clear(data) + if error: + meta["error"] = error + is_error = True + has_changed = False + return (is_error, has_changed, meta) + has_changed = True + + if not has_changed: + meta["present"] = True + is_error = False + return (is_error, has_changed, meta) + + values = [] + for key, value in data["resources"].items(): + values.append("--{0} {1}".format(key, value)) + + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + + command = "dokku resource:limit {0} {1} {2}".format( + " ".join(values), process_type, data["app"] + ) + try: + subprocess.check_call(command, shell=True) + is_error = False + has_changed = True + meta["present"] = True + except subprocess.CalledProcessError as e: + meta["error"] = str(e) + return (is_error, has_changed, meta) + + +def dokku_resource_limit_absent(data): + is_error = True + has_changed = False + meta = {"present": True} + + error = dokku_resource_clear(data) + if error: + meta["error"] = error + is_error = True + has_changed = False + return (is_error, has_changed, meta) + + is_error = False + has_changed = True + meta = {"present": False} + + return (is_error, has_changed, meta) + + +def main(): + fields = { + "app": {"required": True, "type": "str"}, + "process_type": {"required": False, "type": "str"}, + "resources": {"required": False, "type": "dict"}, + "clear_before": {"required": False, "type": "bool"}, + "state": { + "required": False, + "default": "present", + "choices": ["present", "absent"], + "type": "str", + }, + } + choice_map = { + "present": dokku_resource_limit_present, + "absent": dokku_resource_limit_absent, + } + + module = AnsibleModule(argument_spec=fields, supports_check_mode=False) + is_error, has_changed, result = choice_map.get(module.params["state"])( + module.params + ) + + if is_error: + module.fail_json(msg=result["error"], meta=result) + module.exit_json(changed=has_changed, meta=result) + + +if __name__ == "__main__": + main() diff --git a/library/dokku_resource_reserve.py b/library/dokku_resource_reserve.py new file mode 100644 index 0000000..8237ebd --- /dev/null +++ b/library/dokku_resource_reserve.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# from ansible.module_utils.basic import * +from ansible.module_utils.basic import AnsibleModule +import subprocess +import re + +DOCUMENTATION = """ +--- +module: dokku_resource_reserve +short_description: Manage resource reservations for a given dokku application +options: + app: + description: + - The name of the app + required: True + default: null + aliases: [] + resources: + description: + - The Resource type and quantity (required when state=present) + required: False + default: null + aliases: [] + process_type: + description: + - The process type selector + required: False + default: null + alias: [] + clear_before: + description: + - Clear all reserves before apply + required: False + default: "False" + choices: [ "True", "False" ] + aliases: [] + state: + description: + - The state of the resource reservations + required: False + default: present + choices: [ "present", "absent" ] + aliases: [] +author: Alexandre Pavanello e Silva +requirements: [ ] + +""" + +EXAMPLES = """ +- name: Reserve CPU and memory for a dokku app + dokku_resource_reserve: + app: hello-world + resources: + cpu: 100 + memory: 100 + +- name: Create a reservation per process type of a dokku app + dokku_resource_reserve: + app: hello-world + process_type: web + resources: + cpu: 100 + memory: 100 + +- name: Clear all reservations before applying + dokku_resource_reserve: + app: hello-world + state: present + clear_before: True + resources: + cpu: 100 + memory: 100 + +- name: Remove all resource reservations + dokku_resource_reserve: + app: hello-world + state: absent +""" + + +def force_list(var): + if isinstance(var, list): + return var + return list(var) + + +def subprocess_check_output(command, split="\n"): + error = None + output = [] + try: + output = subprocess.check_output(command, shell=True) + if isinstance(output, bytes): + output = output.decode("utf-8") + output = str(output).rstrip("\n") + if split is None: + return output, error + + output = output.split(split) + output = force_list(filter(None, output)) + output = [o.strip() for o in output] + except subprocess.CalledProcessError as e: + error = str(e) + return output, error + + +def dokku_resource_clear(data): + error = None + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + command = "dokku resource:reserve-clear {0} {1}".format(process_type, data["app"]) + try: + subprocess.check_call(command, shell=True) + except subprocess.CalledProcessError as e: + error = str(e) + return error + + +def dokku_resource_reserve_report(data): + + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + command = "dokku --quiet resource:reserve {0} {1}".format(process_type, data["app"]) + + output, error = subprocess_check_output(command) + if error is not None: + return output, error + output = [re.sub(r"\s+", "", line) for line in output] + + report = {} + + for line in output: + if ":" not in line: + continue + key, value = line.split(":", 1) + report[key] = value + + return report, error + + +def dokku_resource_reserve_present(data): + is_error = True + has_changed = False + meta = {"present": False} + + if "resources" not in data: + meta["error"] = "missing required arguments: resources" + return (is_error, has_changed, meta) + + report, error = dokku_resource_reserve_report(data) + if error: + meta["error"] = error + return (is_error, has_changed, meta) + + for k, v in data["resources"].items(): + if k not in report.keys(): + is_error = True + has_changed = False + meta["error"] = "Unknown resource {0}, choose one of: {1}".format( + k, list(report.keys()) + ) + return (is_error, has_changed, meta) + if report[k] != str(v): + has_changed = True + + if data["clear_before"] is True: + + error = dokku_resource_clear(data) + if error: + meta["error"] = error + is_error = True + has_changed = False + return (is_error, has_changed, meta) + has_changed = True + + if not has_changed: + meta["present"] = True + is_error = False + return (is_error, has_changed, meta) + + values = [] + for key, value in data["resources"].items(): + values.append("--{0} {1}".format(key, value)) + + process_type = "" + if data["process_type"]: + process_type = "--process-type {0}".format(data["process_type"]) + + command = "dokku resource:reserve {0} {1} {2}".format( + " ".join(values), process_type, data["app"] + ) + try: + subprocess.check_call(command, shell=True) + is_error = False + has_changed = True + meta["present"] = True + except subprocess.CalledProcessError as e: + meta["error"] = str(e) + return (is_error, has_changed, meta) + + +def dokku_resource_reserve_absent(data): + is_error = True + has_changed = False + meta = {"present": True} + + error = dokku_resource_clear(data) + if error: + meta["error"] = error + is_error = True + has_changed = False + return (is_error, has_changed, meta) + + is_error = False + has_changed = True + meta = {"present": False} + + return (is_error, has_changed, meta) + + +def main(): + fields = { + "app": {"required": True, "type": "str"}, + "process_type": {"required": False, "type": "str"}, + "resources": {"required": False, "type": "dict"}, + "clear_before": {"required": False, "type": "bool"}, + "state": { + "required": False, + "default": "present", + "choices": ["present", "absent"], + "type": "str", + }, + } + choice_map = { + "present": dokku_resource_reserve_present, + "absent": dokku_resource_reserve_absent, + } + + module = AnsibleModule(argument_spec=fields, supports_check_mode=False) + is_error, has_changed, result = choice_map.get(module.params["state"])( + module.params + ) + + if is_error: + module.fail_json(msg=result["error"], meta=result) + module.exit_json(changed=has_changed, meta=result) + + +if __name__ == "__main__": + main()