#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2018, Dag Wieers (dagwieers) # 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 absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: cobbler_system short_description: Manage system objects in Cobbler description: - Add, modify or remove systems in Cobbler extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: full options: host: description: - The name or IP address of the Cobbler system. default: 127.0.0.1 type: str port: description: - Port number to be used for REST connection. - The default value depends on parameter O(use_ssl). type: int username: description: - The username to log in to Cobbler. default: cobbler type: str password: description: - The password to log in to Cobbler. type: str use_ssl: description: - If V(false), an HTTP connection will be used instead of the default HTTPS connection. type: bool default: true validate_certs: description: - If V(false), SSL certificates will not be validated. - This should only set to V(false) when used on personally controlled sites using self-signed certificates. type: bool default: true name: description: - The system name to manage. type: str properties: description: - A dictionary with system properties. type: dict interfaces: description: - A list of dictionaries containing interface options. type: dict sync: description: - Sync on changes. - Concurrently syncing Cobbler is bound to fail. type: bool default: false state: description: - Whether the system should be present, absent or a query is made. choices: [ absent, present, query ] default: present type: str author: - Dag Wieers (@dagwieers) notes: - Concurrently syncing Cobbler is bound to fail with weird errors. - On python 2.7.8 and older (i.e. on RHEL7) you may need to tweak the python behaviour to disable certificate validation. More information at L(Certificate verification in Python standard library HTTP clients,https://access.redhat.com/articles/2039753). ''' EXAMPLES = r''' - name: Ensure the system exists in Cobbler community.general.cobbler_system: host: cobbler01 username: cobbler password: MySuperSecureP4sswOrd name: myhost properties: profile: CentOS6-x86_64 name_servers: [ 2.3.4.5, 3.4.5.6 ] name_servers_search: foo.com, bar.com interfaces: eth0: macaddress: 00:01:02:03:04:05 ipaddress: 1.2.3.4 delegate_to: localhost - name: Enable network boot in Cobbler community.general.cobbler_system: host: bdsol-aci-cobbler-01 username: cobbler password: ins3965! name: bdsol-aci51-apic1.cisco.com properties: netboot_enabled: true state: present delegate_to: localhost - name: Query all systems in Cobbler community.general.cobbler_system: host: cobbler01 username: cobbler password: MySuperSecureP4sswOrd state: query register: cobbler_systems delegate_to: localhost - name: Query a specific system in Cobbler community.general.cobbler_system: host: cobbler01 username: cobbler password: MySuperSecureP4sswOrd name: '{{ inventory_hostname }}' state: query register: cobbler_properties delegate_to: localhost - name: Ensure the system does not exist in Cobbler community.general.cobbler_system: host: cobbler01 username: cobbler password: MySuperSecureP4sswOrd name: myhost state: absent delegate_to: localhost ''' RETURN = r''' systems: description: List of systems returned: O(state=query) and O(name) is not provided type: list system: description: (Resulting) information about the system we are working with returned: when O(name) is provided type: dict ''' import ssl from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import iteritems from ansible.module_utils.six.moves import xmlrpc_client from ansible.module_utils.common.text.converters import to_text from ansible_collections.community.general.plugins.module_utils.datetime import ( now, ) IFPROPS_MAPPING = dict( bondingopts='bonding_opts', bridgeopts='bridge_opts', connected_mode='connected_mode', cnames='cnames', dhcptag='dhcp_tag', dnsname='dns_name', ifgateway='if_gateway', interfacetype='interface_type', interfacemaster='interface_master', ipaddress='ip_address', ipv6address='ipv6_address', ipv6defaultgateway='ipv6_default_gateway', ipv6mtu='ipv6_mtu', ipv6prefix='ipv6_prefix', ipv6secondaries='ipv6_secondariesu', ipv6staticroutes='ipv6_static_routes', macaddress='mac_address', management='management', mtu='mtu', netmask='netmask', static='static', staticroutes='static_routes', virtbridge='virt_bridge', ) def getsystem(conn, name, token): system = dict() if name: # system = conn.get_system(name, token) systems = conn.find_system(dict(name=name), token) if systems: system = systems[0] return system def main(): module = AnsibleModule( argument_spec=dict( host=dict(type='str', default='127.0.0.1'), port=dict(type='int'), username=dict(type='str', default='cobbler'), password=dict(type='str', no_log=True), use_ssl=dict(type='bool', default=True), validate_certs=dict(type='bool', default=True), name=dict(type='str'), interfaces=dict(type='dict'), properties=dict(type='dict'), sync=dict(type='bool', default=False), state=dict(type='str', default='present', choices=['absent', 'present', 'query']), ), supports_check_mode=True, ) username = module.params['username'] password = module.params['password'] port = module.params['port'] use_ssl = module.params['use_ssl'] validate_certs = module.params['validate_certs'] name = module.params['name'] state = module.params['state'] module.params['proto'] = 'https' if use_ssl else 'http' if not port: module.params['port'] = '443' if use_ssl else '80' result = dict( changed=False, ) start = now() ssl_context = None if not validate_certs: try: ssl_context = ssl._create_unverified_context() except AttributeError: # Legacy Python that doesn't verify HTTPS certificates by default pass else: # Handle target environment that doesn't support HTTPS verification ssl._create_default_https_context = ssl._create_unverified_context url = '{proto}://{host}:{port}/cobbler_api'.format(**module.params) if ssl_context: conn = xmlrpc_client.ServerProxy(url, context=ssl_context) else: conn = xmlrpc_client.Server(url) try: token = conn.login(username, password) except xmlrpc_client.Fault as e: module.fail_json(msg="Failed to log in to Cobbler '{url}' as '{username}'. {error}".format(url=url, error=to_text(e), **module.params)) except Exception as e: module.fail_json(msg="Connection to '{url}' failed. {error}".format(url=url, error=to_text(e), **module.params)) system = getsystem(conn, name, token) # result['system'] = system if state == 'query': if name: result['system'] = system else: # Turn it into a dictionary of dictionaries # all_systems = conn.get_systems() # result['systems'] = { system['name']: system for system in all_systems } # Return a list of dictionaries result['systems'] = conn.get_systems() elif state == 'present': if system: # Update existing entry system_id = conn.get_system_handle(name, token) for key, value in iteritems(module.params['properties']): if key not in system: module.warn("Property '{0}' is not a valid system property.".format(key)) if system[key] != value: try: conn.modify_system(system_id, key, value, token) result['changed'] = True except Exception as e: module.fail_json(msg="Unable to change '{0}' to '{1}'. {2}".format(key, value, e)) else: # Create a new entry system_id = conn.new_system(token) conn.modify_system(system_id, 'name', name, token) result['changed'] = True if module.params['properties']: for key, value in iteritems(module.params['properties']): try: conn.modify_system(system_id, key, value, token) except Exception as e: module.fail_json(msg="Unable to change '{0}' to '{1}'. {2}".format(key, value, e)) # Add interface properties interface_properties = dict() if module.params['interfaces']: for device, values in iteritems(module.params['interfaces']): for key, value in iteritems(values): if key == 'name': continue if key not in IFPROPS_MAPPING: module.warn("Property '{0}' is not a valid system property.".format(key)) if not system or system['interfaces'][device][IFPROPS_MAPPING[key]] != value: result['changed'] = True interface_properties['{0}-{1}'.format(key, device)] = value if result['changed'] is True: conn.modify_system(system_id, "modify_interface", interface_properties, token) # Only save when the entry was changed if not module.check_mode and result['changed']: conn.save_system(system_id, token) elif state == 'absent': if system: if not module.check_mode: conn.remove_system(name, token) result['changed'] = True if not module.check_mode and module.params['sync'] and result['changed']: try: conn.sync(token) except Exception as e: module.fail_json(msg="Failed to sync Cobbler. {0}".format(to_text(e))) if state in ('absent', 'present'): result['system'] = getsystem(conn, name, token) if module._diff: result['diff'] = dict(before=system, after=result['system']) elapsed = now() - start module.exit_json(elapsed=elapsed.seconds, **result) if __name__ == '__main__': main()