From 5b07bbfcfc2f271f67e371f5bf488fd64ba7d778 Mon Sep 17 00:00:00 2001 From: Hugh Ma Date: Thu, 22 Dec 2016 05:53:05 -0800 Subject: [PATCH] Stacki host module (#19532) * moving stacki module to remote_management * updating docstring with force_install info --- .../remote_management/stacki/__init__.py | 0 .../remote_management/stacki/stacki_host.py | 308 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 lib/ansible/modules/remote_management/stacki/__init__.py create mode 100644 lib/ansible/modules/remote_management/stacki/stacki_host.py diff --git a/lib/ansible/modules/remote_management/stacki/__init__.py b/lib/ansible/modules/remote_management/stacki/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/modules/remote_management/stacki/stacki_host.py b/lib/ansible/modules/remote_management/stacki/stacki_host.py new file mode 100644 index 0000000000..e4d95febc1 --- /dev/null +++ b/lib/ansible/modules/remote_management/stacki/stacki_host.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Hugh Ma +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +DOCUMENTATION = ''' +--- +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 via API + - U(https://github.com/StackIQ/stacki) +version_added: "2.3" +options: + name: + description: + - Name of the host to be added to Stacki. + required: True + stacki_user: + description: + - Username for authenticating with Stacki API, but if not + specified, the environment variable C(stacki_user) is used instead. + required: True + stacki_password: + description: + - Password for authenticating with Stacki API, but if not + specified, the environment variable C(stacki_password) is used instead. + required: True + stacki_endpoint: + description: + - URL for the Stacki API Endpoint. + required: True + prim_intf_mac: + description: + - MAC Address for the primary PXE boot network interface. + required: False + prim_intf_ip: + description: + - IP Address for the primary network interface. + required: False + prim_intf: + description: + - Name of the primary network interface. + required: False + force_install: + description: + - Set value to True to force node into install state if it already exists in stacki. + requiored: False + +author: "Hugh Ma " +''' + +EXAMPLES = ''' +- name: Add a host named test-1 + 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 + stacki_host: + name: test-1 + stacki_user: usr + stacki_password: pwd + stacki_endpoint: url + state: absent +''' + +RETURN = ''' +changed: + description: response to whether or not the api call completed successfully + returned: always + type: boolean + sample: true + +stdout: + description: the set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: the value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] +''' + +import os +import re +import tempfile +import json +import urllib + + + +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 Intial CSRF + cred_a = self.do_request(self.module, 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 = self.endpoint + "/login" + + # Get Final CSRF and Session ID + login_req = self.do_request(self.module, login_endpoint, headers=header, + payload=urllib.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, module, url, payload=None, headers=None, method=None): + res, info = fetch_url(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.module, self.endpoint, payload=json.dumps({"cmd": "list host"}), + headers=self.header, method="POST") + + if self.hostname in res.read(): + return True + else: + return False + + + def stack_sync(self): + + res = self.do_request(self.module, self.endpoint, payload=json.dumps({ "cmd": "sync config"}), + headers=self.header, method="POST") + + res = self.do_request(self.module, self.endpoint, payload=json.dumps({"cmd": "sync host config"}), + headers=self.header, method="POST") + + + def stack_force_install(self): + + data = dict() + changed = False + + data['cmd'] = "set host boot {0} action=install" \ + .format(self.hostname) + res = self.do_request(self.module, 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_interface(self): + + data['cmd'] = "add host interface {0} interface={1} ip={2} network={3} mac={4} default=true"\ + .format(self.hostname, self.prim_intf, self.prim_intf_ip, self.network, self.prim_intf_mac) + res = self.do_request(self.module, self.endpoint, payload=json.dumps(data), + headers=self.header, method="POST") + + + def stack_add(self, result): + + data = dict() + changed = False + + data['cmd'] = "add host {0} rack={1} rank={2} appliance={3}"\ + .format(self.hostname, self.rack, self.rank, self.appliance) + res = self.do_request(self.module, 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'] = "remove host {0}"\ + .format(self.hostname) + res = self.do_request(self.module, 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=['present', 'absent']), + name=dict(required=True, type='str'), + rack=dict(required=False, type='int', default=0), + rank=dict(required=False, type='int', default=0), + appliance=dict(required=False, type='str', default='backend'), + prim_intf=dict(required=False, type='str', default=None), + prim_intf_ip=dict(required=False, type='str', default=None), + network=dict(required=False, type='str', default='private'), + prim_intf_mac=dict(required=False, type='str', default=None), + stacki_user=dict(required=True, type='str', default=os.environ.get('stacki_user')), + stacki_password=dict(required=True, no_log=True, type='str', default=os.environ.get('stacki_password')), + stacki_endpoint=dict(required=True, type='str', default=os.environ.get('stacki_endpoint')), + force_install=dict(required=False, 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'] = "{0} already exists. Set 'force_install' to true to bootstrap"\ + .format(module.params['name']) + # 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', '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="missing required arguments: {0}".format(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) + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url, ConnectionError + +if __name__ == '__main__': + main()