#!/usr/bin/python # 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 annotations 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 is used instead of the default HTTPS connection. type: bool default: true validate_certs: description: - If V(false), SSL certificates are not 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. """ 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 import xmlrpc.client as xmlrpc_client from ansible.module_utils.basic import AnsibleModule 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 validate_certs or not use_ssl else ssl._create_unverified_context() url = "{proto}://{host}:{port}/cobbler_api".format(**module.params) conn = xmlrpc_client.ServerProxy(url, context=ssl_context) 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=f"Connection to '{url}' failed. {e}") 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 = "" # https://github.com/cobbler/cobbler/blame/v3.3.7/cobbler/api.py#L277 if float(conn.version()) >= 3.4: system_id = conn.get_system_handle(name) else: system_id = conn.get_system_handle(name, token) for key, value in module.params["properties"].items(): if key not in system: module.warn(f"Property '{key}' is not a valid system property.") if system[key] != value: try: conn.modify_system(system_id, key, value, token) result["changed"] = True except Exception as e: module.fail_json(msg=f"Unable to change '{key}' to '{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 module.params["properties"].items(): try: conn.modify_system(system_id, key, value, token) except Exception as e: module.fail_json(msg=f"Unable to change '{key}' to '{value}'. {e}") # Add interface properties interface_properties = dict() if module.params["interfaces"]: for device, values in module.params["interfaces"].items(): for key, value in values.items(): if key == "name": continue if key not in IFPROPS_MAPPING: module.warn(f"Property '{key}' is not a valid system property.") if not system or system["interfaces"][device][IFPROPS_MAPPING[key]] != value: result["changed"] = True interface_properties[f"{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=f"Failed to sync Cobbler. {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()