From 5e496105e3ae434500daa20fe72f138d2669b4f0 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Tue, 17 May 2016 21:28:39 +0200 Subject: Ensure parameters match the provided ones --- foremanhost.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 245 insertions(+), 46 deletions(-) diff --git a/foremanhost.py b/foremanhost.py index e492b8a..47f7094 100644 --- a/foremanhost.py +++ b/foremanhost.py @@ -13,13 +13,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os -import copy - DOCUMENTATION = ''' --- module: foremanhost -short_description: Manages machines in the foreman +short_description: Manages virtual machines in Foreman description: - Manages virtual machines in the I(Foreman). options: @@ -27,11 +24,15 @@ options: description: - name of the machine being managed. required: true + default: null hostgroup: description: - put the machine to the given hostgroup required: false default: null + subnet: + description: + - put the machine into the given subnet api_url: description: - foreman connection url @@ -64,12 +65,13 @@ author: EXAMPLES = ''' # a playbook task line: -- foreman: name=foobar state=present +- foremanhost: name=foobar state=present -# a playbook example of defining and launching machine in the Foreman +# a playbook example of defining and creating a VM via Foreman tasks: - foremanhost: name: foobar + subnet: asubnet state: present json: "{{ lookup('template', 'example.json') }}" api_user: foreman @@ -77,6 +79,11 @@ tasks: api_url: http://localhost:3000/ ''' +import os +import copy + +from ansible.module_utils.basic import * + try: import requests except ImportError: @@ -89,15 +96,20 @@ FOREMAN_FAILED = 1 FOREMAN_SUCCESS = 0 -def merge_json(name, hostgroup_id, customjson): +def merge_json(name, hostgroup_id, image_id, compute_resource_id, subnet_id, customjson): if customjson: customdata = json.loads(customjson) ret = copy.deepcopy(customdata) - if 'host' not in ret: + if not ret.has_key('host'): ret['host'] = {} ret['host']['name'] = name - if hostgroup_id is not None: - ret['host']['hostgroup_id'] = hostgroup_id + ret['host']['hostgroup_id'] = int(hostgroup_id) + if image_id: + ret['host']['image_id'] = int(image_id) + ret['host']['provision_method'] = "image" + ret['host']['compute_resource_id'] = int(compute_resource_id) + if subnet_id: + ret['host']['subnet_id'] = subnet_id return ret @@ -117,8 +129,17 @@ def do_get(url, params): return dict(status=ret.status_code, text=ret.text) -def do_delete(url, data, params): +def do_put(url, data, params): + data = None if data is None else json.dumps(data) + ret = requests.put(url, + data=data, + **params) + ret.raise_for_status() + return dict(status=ret.status_code, text=ret.text) + + +def do_delete(url, params): ret = requests.delete(url, **params) ret.raise_for_status() return dict(status=ret.status_code, text=ret.text) @@ -134,8 +155,8 @@ def is_exists(e): try: err_msg = err["error"]["errors"] # Be careful to avoid IndexError so we can rethrow - # requests exceptoin - if ("name" in err_msg("name") and + # requests exception + if (err_msg.has_key("name") and err_msg["name"] == [u'has already been taken']): return True except IndexError: @@ -145,87 +166,265 @@ def is_exists(e): def is_absent(e): """ - Check if the error returned indicates an missing entity + Check if the error returned indicates a missing entity """ if e.response.status_code == 404: return True return False -def item_to_id(base_url, field, name, params): +def item_to_id(base_url, field, name): + ret = find_item(base_url, field, name) + return '%d' % ret['id'] if ret else None + + +def find_item(base_url, field, name): url = base_url + "?search=%s=\"%s\"" % (field, name) - ret = do_get(url, params) + ret = do_get(url, headers) results = json.loads(ret['text'])['results'] if results == []: return None if len(results) > 1: raise ValueError("Found more than item for '%s'" % name) - return "%s" % results[0]['id'] + return results[0] + +def find_image(compute_resource_id, image): + image_url = "%s/api/v2/compute_resources/%s/images" % (api_url, compute_resource_id) + image_id = item_to_id(image_url, 'name', image) + if not image_id: + raise ValueError("Image '%s' not found" % image) + return image_id + + +def find_compute_resource(compute_resource): + compute_resource_url = "%s/api/v2/compute_resources/" % api_url + compute_resource_id = item_to_id(compute_resource_url, 'name', compute_resource) + if not compute_resource_id: + raise ValueError("Compute resource '%s' not found" % compute_resource) + return compute_resource_id + + +def find_subnet(name): + subnet_url = "%s/api/v2/subnets/" % api_url + subnet = find_item(subnet_url, 'name', name) + if not subnet: + raise ValueError("Subnet '%s' not found" % subnet) + return subnet + + +def find_subnet_domains(subnet_id): + subnet_domain_url = "%s/api/subnets/%s/domains" % (api_url, subnet_id) + ret = do_get(subnet_domain_url, headers) + return json.loads(ret['text'])['results'] + + +def find_host(host, subnet): + host_url = "%s/api/v2/hosts" % api_url + + domains = find_subnet_domains(subnet['id']) + if domains == []: + return None + if len(domains) > 1: + raise ValueError("Found more than one domain for subnet '%s'" % subnet['name']) + fqdn = "%s.%s" % (host, domains[0]['name']) + + host_id = item_to_id(host_url, 'name', fqdn) + if not host_id: + raise ValueError("Host '%s' not found" % host) + return host_id + + +def get_params(hid): + hostparam_url = "%s/api/v2/hosts/%s/parameters" % (api_url, hid) + ret = do_get(hostparam_url, headers) + return json.loads(ret['text'])['results'] + + +def param_by_name(name, params): + """ + Lookup a parameter by name in the parameter list returned by foreman + + >>> param_by_name("foo", [{"name": "foo", + ... "value": "fasel"}])['value'] + 'fasel' + + >>> param_by_name("foo", [{"name": "bla", + ... "value": "fasel"}]) + Traceback (most recent call last): + ... + ValueError: No Param with name foo found in [{'name': 'bla', 'value': 'fasel'}] + """ + for p in params: + if p['name'] == name: + return p + raise ValueError("No Param with name %s found in %s" % (name, params)) + + +def add_param(hid, name, value): + hostparam_url = "%s/api/v2/hosts/%s/parameters" % (api_url, hid) + p = { + "parameter": { + "name": name, + "value": value, + }, + } + do_post(hostparam_url, p, headers) + + +def del_param(hid, name, foreman_params): + param_id = param_by_name(name, foreman_params)['id'] + hostparam_url = "%s/api/v2/hosts/%s/parameters/%s" % (api_url, hid, param_id) + do_delete(hostparam_url, headers) + + +def update_param(hid, name, value, foreman_params): + param_id = param_by_name(name, foreman_params)['id'] + hostparam_url = "%s/api/v2/hosts/%s/parameters/%s" % (api_url, hid, param_id) + p = { + "parameter": { + "name": name, + "value": value, + }, + } + do_put(hostparam_url, p, headers) + + +def ensure_params(hid, parameters): + """Make sure the params given match the ones tagged onto the + foreman host""" + foreman_params = get_params(hid) + new = parameters.keys() + old = [p['name'] for p in foreman_params] + changed = False + + add_params = set(new) - set(old) + del_params = set(old) - set(new) + mod_params = set(new).intersection(old) + + for name in add_params: + add_param(hid, name, parameters[name]) + changed = True + + for name in mod_params: + cur_value = param_by_name(name, foreman_params)['value'] + if parameters[name] != cur_value: + update_param(hid, name, parameters[name], foreman_params) + changed = True + + for name in del_params: + del_param(hid, name, foreman_params) + changed = True + + return changed + def core(module): + global headers + global api_url + name = module.params.get('name', None) hostgroup = module.params.get('hostgroup', None) + image = module.params.get('image', None) + compute_resource = module.params.get('compute_resource', None) + subnetname = module.params.get('subnet', None) state = module.params.get('state', 'present') customjson = module.params.get('json', None) + parameters = module.params.get('params', []) api_url = module.params.get('api_url', None) api_user = module.params.get('api_user', None) api_pw = module.params.get('api_password', os.getenv("ANSIBLE_FOREMAN_PW")) ssl_verify = module.params.get('ssl_verify', True) + image_id = None + subnet = None + changed = False ret = {} - hostgroup_id = None host_url = "%s/api/v2/hosts" % api_url - params = {'headers': {'Content-Type': 'application/json'}, - 'auth': HTTPBasicAuth(api_user, api_pw), - 'verify': ssl_verify, + headers = { 'headers': {'Content-Type': 'application/json'}, + 'auth': HTTPBasicAuth(api_user, api_pw), + 'verify': ssl_verify, } if state == 'present': - if hostgroup: - hostgroup_url = "%s/api/v2/hostgroups" % api_url - hostgroup_id = item_to_id(hostgroup_url, 'title', hostgroup, params) - if not hostgroup_id: - raise ValueError("Hostgroup '%s' not found for '%s'" % (hostgroup, name)) - fulljson = merge_json(name, hostgroup_id, customjson) + hostgroup_url = "%s/api/v2/hostgroups" % api_url + hostgroup_id = item_to_id(hostgroup_url, 'title', hostgroup) + if not hostgroup_id: + raise ValueError("Hostgroup '%s' not found for '%s'" % (hostgroup, name)) + compute_resource_id = find_compute_resource(compute_resource) + if image: + image_id = find_image(compute_resource_id, image) + subnet = find_subnet(subnetname) + fulljson = merge_json(name, + hostgroup_id, + image_id, + compute_resource_id, + subnet['id'], + customjson) try: - ret = do_post(host_url, fulljson, params) - ret['changed'] = True + ret = do_post(host_url, fulljson, headers) + hid = json.loads(ret['text'])['id'] + changed = True except requests.exceptions.HTTPError as e: if is_exists(e): - ret['changed'] = False + hid = item_to_id(host_url, 'name', name) else: - raise + try: + module.fail_json( + msg='Failed to create host %s: %s' % (name, e.response.json()['error']['full_messages']) + ) + # Catch any failures to get an error message + except Exception as f: + module.fail_json( + msg='Failed to create host %s: %s (failed to parse detailed error output: %s' % (name, e, f) + ) + + if not hid: + if not subnet: + subnet = find_subnet(subnetname) + hid = find_host(name, subnet) + + if not hid: + raise ValueError("Host %s not found" % name) + + if ensure_params(hid, parameters): + changed = True + elif state == 'absent': - host_id = item_to_id(host_url, 'name', name, params) + host_id = item_to_id(host_url, 'name', name) if host_id is None: ret['changed'] = False return FOREMAN_SUCCESS, ret try: url = os.path.join(host_url, host_id) - ret = do_delete(url, None, params) - ret['changed'] = True + ret = do_delete(url, headers) + changed = True except requests.exceptions.HTTPError as e: if is_absent(e): - ret['changed'] = False + changed = False else: raise else: raise ValueError("Unknown state %s" % state) + ret['changed'] = changed return FOREMAN_SUCCESS, ret def main(): module = AnsibleModule(argument_spec=dict( - name=dict(required=True), - hostgroup=dict(), - json=dict(), - state=dict(default='present', choices=['present', 'absent']), - api_url=dict(required=True), - api_user=dict(required=True), - api_password=dict(), - ssl_verify=dict(), + name = dict(required=True), + hostgroup = dict(type='str'), + image = dict(type='str'), + compute_resource = dict(type='str'), + subnet = dict(type='str'), + params = dict(type='dict'), + json = dict(), + state = dict(default='present', choices=['present','absent']), + api_url = dict(required=True), + api_user = dict(required=True), + api_password = dict(), + ssl_verify = dict(), )) if not HAS_REQUESTS: @@ -245,6 +444,6 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + # import module snippets + main() -- cgit v1.2.3