diff --git a/changelogs/fragments/797-proxmox-kvm-cloud-init.yaml b/changelogs/fragments/797-proxmox-kvm-cloud-init.yaml new file mode 100644 index 0000000000..759746d455 --- /dev/null +++ b/changelogs/fragments/797-proxmox-kvm-cloud-init.yaml @@ -0,0 +1,2 @@ +minor_changes: + - "proxmox_kvm - add cloud-init support (new options: ``cicustom``, ``cipassword``, ``citype``, ``ciuser``, ``ipconfig``, ``nameservers``, ``searchdomains``, ``sshkeys``) (https://github.com/ansible-collections/community.general/pull/797)." diff --git a/plugins/modules/cloud/misc/proxmox_kvm.py b/plugins/modules/cloud/misc/proxmox_kvm.py index 3c29fab442..7fd2c203e1 100644 --- a/plugins/modules/cloud/misc/proxmox_kvm.py +++ b/plugins/modules/cloud/misc/proxmox_kvm.py @@ -89,6 +89,29 @@ options: description: - Enable booting from specified disk. C((ide|sata|scsi|virtio)\d+) type: str + cicustom: + description: + - 'cloud-init: Specify custom files to replace the automatically generated ones at start.' + type: str + version_added: 1.3.0 + cipassword: + description: + - 'cloud-init: password of default user to create.' + type: str + version_added: 1.3.0 + citype: + description: + - 'cloud-init: Specifies the cloud-init configuration format.' + - The default depends on the configured operating system type (C(ostype)). + - We use the C(nocloud) format for Linux, and C(configdrive2) for Windows. + type: str + choices: ['nocloud', 'configdrive2'] + version_added: 1.3.0 + ciuser: + description: + - 'cloud-init: username of default user to create.' + type: str + version_added: 1.3.0 clone: description: - Name of VM to be cloned. If C(vmid) is setted, C(clone) can take arbitrary value but required for initiating the clone. @@ -195,6 +218,19 @@ options: - C(size) is the size of the disk in GB. - C(format) is the drive's backing file's data format. C(qcow2|raw|subvol). type: dict + ipconfig: + description: + - 'cloud-init: Set the IP configuration.' + - A hash/dictionary of network ip configurations. C(ipconfig='{"key":"value", "key":"value"}'). + - Keys allowed are - C(ipconfig[n]) where 0 ≤ n ≤ network interfaces. + - Values allowed are - C("[gw=] [,gw6=] [,ip=] [,ip6=]"). + - 'cloud-init: Specify IP addresses and gateways for the corresponding interface.' + - IP addresses use CIDR notation, gateways are optional but they should be in the same subnet of specified IP address. + - The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided. + - For IPv6 the special string 'auto' can be used to use stateless autoconfiguration. + - If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4. + type: dict + version_added: 1.3.0 keyboard: description: - Sets the keyboard layout for VNC server. @@ -242,6 +278,13 @@ options: - Specifies the VM name. Only used on the configuration web interface. - Required only for C(state=present). type: str + nameservers: + description: + - 'cloud-init: DNS server IP address(es).' + - If unset, PVE host settings are used. + type: list + elements: str + version_added: 1.3.0 net: description: - A hash/dictionary of network interfaces for the VM. C(net='{"key":"value", "key":"value"}'). @@ -339,6 +382,13 @@ options: - Specifies the SCSI controller model. type: str choices: ['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi'] + searchdomains: + description: + - 'cloud-init: Sets DNS search domain(s).' + - If unset, PVE host settings are used. + type: list + elements: str + version_added: 1.3.0 serial: description: - A hash/dictionary of serial device to create inside the VM. C('{"key":"value", "key":"value"}'). @@ -373,6 +423,11 @@ options: option has a default of C(1). Note that the default value of I(proxmox_default_behavior) changes in community.general 4.0.0. type: int + sshkeys: + description: + - 'cloud-init: SSH key to assign to the default user. NOT TESTED with multiple keys but a multi-line value should work.' + type: str + version_added: 1.3.0 startdate: description: - Sets the initial date of the real time clock. @@ -513,7 +568,9 @@ EXAMPLES = ''' api_host: helldorado name: spynal node: sabrewulf - net: '{"net0":"virtio,bridge=vmbr1,rate=200", "net1":"e1000,bridge=vmbr2,"}' + net: + net0: 'virtio,bridge=vmbr1,rate=200' + net1: 'e1000,bridge=vmbr2' - name: Create new VM with one network interface, three virto hard disk, 4 cores, and 2 vcpus community.general.proxmox_kvm: @@ -522,8 +579,12 @@ EXAMPLES = ''' api_host: helldorado name: spynal node: sabrewulf - net: '{"net0":"virtio,bridge=vmbr1,rate=200"}' - virtio: '{"virtio0":"VMs_LVM:10", "virtio1":"VMs:2,format=qcow2", "virtio2":"VMs:5,format=raw"}' + net: + net0: 'virtio,bridge=vmbr1,rate=200' + virtio: + virtio0: 'VMs_LVM:10' + virtio1: 'VMs:2,format=qcow2' + virtio2: 'VMs:5,format=raw' cores: 4 vcpus: 2 @@ -572,7 +633,7 @@ EXAMPLES = ''' format: raw timeout: 300 -- name: Create new VM and lock it for snapashot +- name: Create new VM and lock it for snapshot community.general.proxmox_kvm: api_user: root@pam api_password: secret @@ -590,6 +651,43 @@ EXAMPLES = ''' node: sabrewulf protection: yes +- name: Create new VM using cloud-init with a username and password + community.general.proxmox_kvm: + node: sabrewulf + api_user: root@pam + api_password: secret + api_host: helldorado + name: spynal + ide: + ide2: 'local:cloudinit,format=qcow2' + ciuser: mylinuxuser + cipassword: supersecret + searchdomains: 'mydomain.internal' + nameservers: 1.1.1.1 + net: + net0: 'virtio,bridge=vmbr1,tag=77' + ipconfig: + ipconfig0: 'ip=192.168.1.1/24,gw=192.168.1.1' + +- name: Create new VM using Cloud-Init with an ssh key + community.general.proxmox_kvm: + node: sabrewulf + api_user: root@pam + api_password: secret + api_host: helldorado + name: spynal + ide: + ide2: 'local:cloudinit,format=qcow2' + sshkeys: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILJkVm98B71lD5XHfihwcYHE9TVpsJmK1vR1JcaU82L+' + searchdomains: 'mydomain.internal' + nameservers: + - '1.1.1.1' + - '8.8.8.8' + net: + net0: 'virtio,bridge=vmbr1,tag=77' + ipconfig: + ipconfig0: 'ip=192.168.1.1/24' + - name: Start VM community.general.proxmox_kvm: api_user: root@pam @@ -720,6 +818,7 @@ import re import time import traceback from distutils.version import LooseVersion +from ansible.module_utils.six.moves.urllib.parse import quote try: from proxmoxer import ProxmoxAPI @@ -823,6 +922,7 @@ def wait_for_task(module, proxmox, node, taskid): def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sockets, update, **kwargs): # Available only in PVE 4 only_v4 = ['force', 'protection', 'skiplock'] + only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig'] # valide clone parameters valid_clone_params = ['format', 'full', 'pool', 'snapname', 'storage', 'target'] @@ -836,12 +936,23 @@ def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sock kwargs = dict((k, v) for k, v in kwargs.items() if v is not None) kwargs.update(dict([k, int(v)] for k, v in kwargs.items() if isinstance(v, bool))) - # The features work only on PVE 4 + # The features work only on PVE 4+ if PVE_MAJOR_VERSION < 4: for p in only_v4: if p in kwargs: del kwargs[p] + # The features work only on PVE 6 + if PVE_MAJOR_VERSION < 6: + for p in only_v6: + if p in kwargs: + del kwargs[p] + + # 'sshkeys' param expects an urlencoded string + if 'sshkeys' in kwargs: + urlencoded_ssh_keys = quote(kwargs['sshkeys'], safe='') + kwargs['sshkeys'] = str(urlencoded_ssh_keys) + # If update, don't update disk (virtio, ide, sata, scsi) and network interface # pool parameter not supported by qemu//config endpoint on "update" (PVE 6.2) - only with "create" if update: @@ -860,17 +971,27 @@ def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sock if 'pool' in kwargs: del kwargs['pool'] - # Convert all dict in kwargs to elements. For hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n] + # Convert all dict in kwargs to elements. For hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n], ipconfig[n] for k in list(kwargs.keys()): if isinstance(kwargs[k], dict): kwargs.update(kwargs[k]) del kwargs[k] - # Rename numa_enabled to numa. According the API documentation + # Rename numa_enabled to numa. According the API documentation if 'numa_enabled' in kwargs: kwargs['numa'] = kwargs['numa_enabled'] del kwargs['numa_enabled'] + # PVE api expects strings for the following params + if 'nameservers' in module.params: + nameservers = module.params.pop('nameservers') + if nameservers: + kwargs['nameserver'] = ' '.join(nameservers) + if 'searchdomains' in module.params: + searchdomains = module.params.pop('searchdomains') + if searchdomains: + kwargs['searchdomain'] = ' '.join(searchdomains) + # -args and skiplock require root@pam user if module.params['api_user'] == "root@pam" and module.params['args'] is None: if not update: @@ -947,6 +1068,10 @@ def main(): bios=dict(choices=['seabios', 'ovmf']), boot=dict(type='str'), bootdisk=dict(type='str'), + cicustom=dict(type='str'), + cipassword=dict(type='str', no_log=True), + citype=dict(type='str', choices=['nocloud', 'configdrive2']), + ciuser=dict(type='str'), clone=dict(type='str', default=None), cores=dict(type='int'), cpu=dict(type='str'), @@ -963,6 +1088,7 @@ def main(): hotplug=dict(type='str'), hugepages=dict(choices=['any', '2', '1024']), ide=dict(type='dict'), + ipconfig=dict(type='dict'), keyboard=dict(type='str'), kvm=dict(type='bool'), localtime=dict(type='bool'), @@ -972,6 +1098,7 @@ def main(): migrate_downtime=dict(type='int'), migrate_speed=dict(type='int'), name=dict(type='str'), + nameservers=dict(type='list', elements='str'), net=dict(type='dict'), newid=dict(type='int', default=None), node=dict(), @@ -988,11 +1115,13 @@ def main(): scsi=dict(type='dict'), scsihw=dict(choices=['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi']), serial=dict(type='dict'), + searchdomains=dict(type='list', elements='str'), shares=dict(type='int'), skiplock=dict(type='bool'), smbios=dict(type='str'), snapname=dict(type='str'), sockets=dict(type='int'), + sshkeys=dict(type='str'), startdate=dict(type='str'), startup=dict(), state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted', 'current']), @@ -1078,7 +1207,7 @@ def main(): if not api_password: try: api_password = os.environ['PROXMOX_PASSWORD'] - except KeyError as e: + except KeyError: module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') auth_args['password'] = api_password else: @@ -1088,7 +1217,8 @@ def main(): try: proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args) global PVE_MAJOR_VERSION - PVE_MAJOR_VERSION = 3 if proxmox_version(proxmox) < LooseVersion('4.0') else 4 + version = proxmox_version(proxmox) + PVE_MAJOR_VERSION = 3 if version < LooseVersion('4.0') else version.version[0] except Exception as e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) @@ -1098,13 +1228,13 @@ def main(): if state == 'present' and not update and not clone and not delete and not revert: try: vmid = get_nextvmid(module, proxmox) - except Exception as e: + except Exception: module.fail_json(msg="Can't get the next vmid for VM {0} automatically. Ensure your cluster state is good".format(name)) else: clone_target = clone or name try: vmid = get_vmid(proxmox, clone_target)[0] - except Exception as e: + except Exception: vmid = -1 if clone is not None: @@ -1112,7 +1242,7 @@ def main(): if not newid: try: newid = get_nextvmid(module, proxmox) - except Exception as e: + except Exception: module.fail_json(msg="Can't get the next vmid for VM {0} automatically. Ensure your cluster state is good".format(name)) # Ensure source VM name exists when cloning @@ -1164,6 +1294,10 @@ def main(): bios=module.params['bios'], boot=module.params['boot'], bootdisk=module.params['bootdisk'], + cicustom=module.params['cicustom'], + cipassword=module.params['cipassword'], + citype=module.params['citype'], + ciuser=module.params['ciuser'], cpulimit=module.params['cpulimit'], cpuunits=module.params['cpuunits'], description=module.params['description'], @@ -1174,6 +1308,7 @@ def main(): hotplug=module.params['hotplug'], hugepages=module.params['hugepages'], ide=module.params['ide'], + ipconfig=module.params['ipconfig'], keyboard=module.params['keyboard'], kvm=module.params['kvm'], localtime=module.params['localtime'], @@ -1198,6 +1333,7 @@ def main(): skiplock=module.params['skiplock'], smbios1=module.params['smbios'], snapname=module.params['snapname'], + sshkeys=module.params['sshkeys'], startdate=module.params['startdate'], startup=module.params['startup'], tablet=module.params['tablet'],