#!/usr/bin/python # Copyright (c) 2016, Hugh Ma # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: stacki_host short_description: Add or remove host to stacki front-end description: - Use this module to add or remove hosts to a stacki front-end using API. - Information on stacki can be found at U(https://github.com/StackIQ/stacki). extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: none diff_mode: support: none options: name: description: - Name of the host to be added to Stacki. required: true type: str stacki_user: description: - Username for authenticating with Stacki API, but if not specified, the environment variable E(stacki_user) is used instead. required: true type: str stacki_password: description: - Password for authenticating with Stacki API, but if not specified, the environment variable E(stacki_password) is used instead. required: true type: str stacki_endpoint: description: - URL for the Stacki API Endpoint. required: true type: str prim_intf_mac: description: - MAC Address for the primary PXE boot network interface. - Currently not used by the module. type: str prim_intf_ip: description: - IP Address for the primary network interface. - Currently not used by the module. type: str prim_intf: description: - Name of the primary network interface. - Currently not used by the module. type: str force_install: description: - Set value to V(true) to force node into install state if it already exists in stacki. type: bool default: false state: description: - Set value to the desired state for the specified host. type: str choices: [absent, present] default: present appliance: description: - Appliance to be used in host creation. - Required if O(state=present) and host does not yet exist. type: str default: backend rack: description: - Rack to be used in host creation. - Required if O(state=present) and host does not yet exist. type: int default: 0 rank: description: - Rank to be used in host creation. - In Stacki terminology, the rank is the position of the machine in a rack. - Required if O(state=present) and host does not yet exist. type: int default: 0 network: description: - Network to be configured in the host. - Currently not used by the module. type: str default: private author: - Hugh Ma (@bbyhuy) """ EXAMPLES = r""" - name: Add a host named test-1 community.general.stacki_host: name: test-1 stacki_user: usr stacki_password: pwd stacki_endpoint: url prim_intf_mac: mac_addr prim_intf_ip: x.x.x.x prim_intf: eth0 - name: Remove a host named test-1 community.general.stacki_host: name: test-1 stacki_user: usr stacki_password: pwd stacki_endpoint: url state: absent """ import json from ansible.module_utils.basic import AnsibleModule, env_fallback from urllib.parse import urlencode from ansible.module_utils.urls import fetch_url class StackiHost: def __init__(self, module): self.module = module self.hostname = module.params["name"] self.rack = module.params["rack"] self.rank = module.params["rank"] self.appliance = module.params["appliance"] self.prim_intf = module.params["prim_intf"] self.prim_intf_ip = module.params["prim_intf_ip"] self.network = module.params["network"] self.prim_intf_mac = module.params["prim_intf_mac"] self.endpoint = module.params["stacki_endpoint"] auth_creds = {"USERNAME": module.params["stacki_user"], "PASSWORD": module.params["stacki_password"]} # Get Initial CSRF cred_a = self.do_request(self.endpoint, method="GET") cookie_a = cred_a.headers.get("Set-Cookie").split(";") init_csrftoken = None for c in cookie_a: if "csrftoken" in c: init_csrftoken = c.replace("csrftoken=", "") init_csrftoken = init_csrftoken.rstrip("\r\n") break # Make Header Dictionary with initial CSRF header = { "csrftoken": init_csrftoken, "X-CSRFToken": init_csrftoken, "Content-type": "application/x-www-form-urlencoded", "Cookie": cred_a.headers.get("Set-Cookie"), } # Endpoint to get final authentication header login_endpoint = f"{self.endpoint}/login" # Get Final CSRF and Session ID login_req = self.do_request(login_endpoint, headers=header, payload=urlencode(auth_creds), method="POST") cookie_f = login_req.headers.get("Set-Cookie").split(";") csrftoken = None for f in cookie_f: if "csrftoken" in f: csrftoken = f.replace("csrftoken=", "") if "sessionid" in f: sessionid = c.split("sessionid=", 1)[-1] sessionid = sessionid.rstrip("\r\n") self.header = { "csrftoken": csrftoken, "X-CSRFToken": csrftoken, "sessionid": sessionid, "Content-type": "application/json", "Cookie": login_req.headers.get("Set-Cookie"), } def do_request(self, url, payload=None, headers=None, method=None): res, info = fetch_url(self.module, url, data=payload, headers=headers, method=method) if info["status"] != 200: self.module.fail_json(changed=False, msg=info["msg"]) return res def stack_check_host(self): res = self.do_request( self.endpoint, payload=json.dumps({"cmd": "list host"}), headers=self.header, method="POST" ) return self.hostname in res.read() def stack_sync(self): self.do_request(self.endpoint, payload=json.dumps({"cmd": "sync config"}), headers=self.header, method="POST") self.do_request( self.endpoint, payload=json.dumps({"cmd": "sync host config"}), headers=self.header, method="POST" ) def stack_force_install(self, result): data = {"cmd": f"set host boot {self.hostname} action=install"} self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST") changed = True self.stack_sync() result["changed"] = changed result["stdout"] = "api call successful".rstrip("\r\n") def stack_add(self, result): data = dict() changed = False data["cmd"] = f"add host {self.hostname} rack={self.rack} rank={self.rank} appliance={self.appliance}" self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST") self.stack_sync() result["changed"] = changed result["stdout"] = "api call successful".rstrip("\r\n") def stack_remove(self, result): data = dict() data["cmd"] = f"remove host {self.hostname}" self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST") self.stack_sync() result["changed"] = True result["stdout"] = "api call successful".rstrip("\r\n") def main(): module = AnsibleModule( argument_spec=dict( state=dict(type="str", default="present", choices=["absent", "present"]), name=dict(type="str", required=True), rack=dict(type="int", default=0), rank=dict(type="int", default=0), appliance=dict(type="str", default="backend"), prim_intf=dict(type="str"), prim_intf_ip=dict(type="str"), network=dict(type="str", default="private"), prim_intf_mac=dict(type="str"), stacki_user=dict(type="str", required=True, fallback=(env_fallback, ["stacki_user"])), stacki_password=dict(type="str", required=True, fallback=(env_fallback, ["stacki_password"]), no_log=True), stacki_endpoint=dict(type="str", required=True, fallback=(env_fallback, ["stacki_endpoint"])), force_install=dict(type="bool", default=False), ), supports_check_mode=False, ) result = {"changed": False} missing_params = list() stacki = StackiHost(module) host_exists = stacki.stack_check_host() # If state is present, but host exists, need force_install flag to put host back into install state if module.params["state"] == "present" and host_exists and module.params["force_install"]: stacki.stack_force_install(result) # If state is present, but host exists, and force_install and false, do nothing elif module.params["state"] == "present" and host_exists and not module.params["force_install"]: result["stdout"] = f"{module.params['name']} already exists. Set 'force_install' to true to bootstrap" # Otherwise, state is present, but host doesn't exists, require more params to add host elif module.params["state"] == "present" and not host_exists: for param in ["appliance", "rack", "rank", "prim_intf", "prim_intf_ip", "network", "prim_intf_mac"]: if not module.params[param]: missing_params.append(param) if len(missing_params) > 0: module.fail_json(msg=f"missing required arguments: {missing_params}") stacki.stack_add(result) # If state is absent, and host exists, lets remove it. elif module.params["state"] == "absent" and host_exists: stacki.stack_remove(result) module.exit_json(**result) if __name__ == "__main__": main()