diff --git a/pvm-ansible/Pulumi.yaml b/pvm-ansible/Pulumi.yaml index 6bce2c2..8ece092 100644 --- a/pvm-ansible/Pulumi.yaml +++ b/pvm-ansible/Pulumi.yaml @@ -6,6 +6,59 @@ runtime: python template: description: ProxmoxVE provider, Ansible powered with Python config: - password: - description: Password of the root user over PVE. + pveHostname: + type: String + default: pve.vavassori.lcl + description: PVE hostname + pveNodeName: + type: String + default: pve + description: Node of the Proxmox VE to which write the VM. + pveVerifySSL: + type: Number + default: 0 + description: Verify SSL certificate of PVE Node + pveTokenName: + type: String + default: root@pam!automata + description: Authentication token for PVE Node + pveTokenValue: + type: String secret: true + description: Authentication token value for PVE Node + pveDefaultStorage: + type: String + default: local-lvm + description: Default storage in which put the VMs + hostname: + type: String + description: Hostname of the new machine + vmTemplate: + type: String + default: debian12 + description: OS name of the needed template + vmSockets: + type: Number + default: 1 + description: Number of sockets of the new VMs + vmCores: + type: Number + default: 1 + description: Number of cores per socket of the new VM + vmRAM: + type: Number + default: 2048 + description: Number of MBs of RAM of the new VM + vmDiskSize: + type: Number + default: 20 + description: Number of GBs of the VM disk + cloudUsername: + type: String + default: syntaxerrormmm + description: Username created in the cloud-init phase + cloudPassword: + type: String + default: cicciopasticcio + secret: true + description: Password of the username created in the cloud-init phase. diff --git a/pvm-ansible/__main__.py b/pvm-ansible/__main__.py index 5396344..313d9a3 100644 --- a/pvm-ansible/__main__.py +++ b/pvm-ansible/__main__.py @@ -3,50 +3,59 @@ import pulumi import pulumi_proxmoxve as proxmox import pulumi_command as command -import yaml -from dotmap import DotMap - -# Import configuration parameters from input.yaml -input_ = DotMap(yaml.safe_load(open("./input.yaml", "r").read())) +from pvewrapper import PveWrapper +import additional_config # Check pulumi configuration config = pulumi.Config() -provider = proxmox.Provider("proxmoxve", - endpoint=input_.pve.url, - insecure=input_.pve.insecure, - username=input_.pve.username, - password=config.require_secret("password") +username = config.require("pveTokenName").split("!")[0] +tokenname = config.require("pveTokenName").split("!")[1] +pvehostname = config.require("pveHostname") +pvenodename = config.require("pveNodeName") +pveURL=f"https://{pvehostname}:8006/" +verify = True if config.get_int("pveVerifySSL") == 1 else False +pvedefaultstorage = config.require("pveDefaultStorage") +vm_name: str = config.require("hostname") +vm_username: str = config.require("cloudUsername") + +provider = proxmox.Provider(pvehostname, + endpoint=pveURL, + insecure=not verify, + api_token=config.require_secret("pveTokenValue").apply(lambda x: f"{username}!{tokenname}={x}") ) -vm_name = input_.vm.name -vm_username = input_.vm.username -ansible_playbook = input_.ansible_playbook +pve = PveWrapper(pvehostname, + username=username, + token_name=tokenname, + token_value=config.require_secret("pveTokenValue").apply(lambda v: f"{v}"), + nodename=pvenodename, + verify_ssl=verify +) -vm = proxmox.vm.VirtualMachine("vm", +vm = proxmox.vm.VirtualMachine(vm_name, name = vm_name, - node_name = input_.pve.nodename, + node_name = pvenodename, agent = proxmox.vm.VirtualMachineAgentArgs( enabled = True, trim = True, type = "virtio" ), - bios = "seabios", cpu = proxmox.vm.VirtualMachineCpuArgs( - cores = input_.vm.cores, - sockets = input_.vm.sockets + cores = config.get_int("vmSockets"), + sockets = config.get_int("vmCores") ), - memory = proxmox.vm.VirtualMachineMemoryArgs(dedicated = input_.vm.ram), + memory = proxmox.vm.VirtualMachineMemoryArgs(dedicated = config.get_int("vmRAM")), clone = proxmox.vm.VirtualMachineCloneArgs( - node_name = input_.pve.nodename, - vm_id = input_.vm.clone, - full = True + node_name = pvenodename, + vm_id = additional_config.os_to_template[config.require("vmTemplate")], + full = False ), disks = [ proxmox.vm.VirtualMachineDiskArgs( interface = "virtio0", - datastore_id = input_.pve.storage, - size = input_.vm.disksize, + datastore_id = pvedefaultstorage, + size = config.get_int("vmDiskSize"), file_format = "raw" ) ], @@ -56,17 +65,16 @@ vm = proxmox.vm.VirtualMachine("vm", model = "virtio" ) ], - on_boot = True, operating_system = proxmox.vm.VirtualMachineOperatingSystemArgs( type = "l26" ), initialization = proxmox.vm.VirtualMachineInitializationArgs( type = "nocloud", - datastore_id = input_.pve.storage, + datastore_id = pvedefaultstorage, user_account = proxmox.vm.VirtualMachineInitializationUserAccountArgs( username = vm_username, - password = input_.vm.password, - keys = input_.vm.sshkeys + password = config.require_secret("cloudPassword"), + keys = additional_config.sshkeys ) ), opts = pulumi.ResourceOptions( @@ -75,27 +83,27 @@ vm = proxmox.vm.VirtualMachine("vm", ) ) -# First item of the ipv4_addresses is of the loopback interface (so the usual 127.0.0.1). Let's get the second and grab only the text. +ipv4_addresses = vm.vm_id.apply(lambda vm_id: pve.ipv4_addresses(f"{vm_id}")) # Creating the inventory file inventory = command.local.Command("a-inventory", - create = vm.ipv4_addresses[1][0].apply( - lambda ipaddr: f"echo '{vm_name} ansible_host={ipaddr} ansible_user={vm_username}' >./inventory" + create = vm.ipv4_addresses.apply(lambda ipaddr: + f"echo '{vm_name} ansible_host={ipaddr[0]['ipv4_address']} ansible_user={vm_username}' >./inventory" ), delete = "rm -f ./inventory", - opts = pulumi.ResourceOptions(depends_on = [ vm ]) + opts = pulumi.ResourceOptions(depends_on = [ ipv4_addresses ]) ) -# Try the deployment with ansible - # Applying the command -execute_ansible = command.local.Command("a-deploy", - create = vm.ipv4_addresses[1][0].apply( - lambda ipaddr: f"ansible-playbook {ansible_playbook}" - ), - delete = "rm -f ./ansible.log", - opts = pulumi.ResourceOptions(depends_on = [ inventory ]) -) +for playbook in additional_config.playbooks: + shortname: str = playbook.split(".")[0] + command.local.Command(f"ap-{shortname}", + create = vm.ipv4_addresses.apply( + lambda run: f"ansible-playbook {run[0]['ipv4_address']}" + ), + delete = "rm -f ./ansible.log", + opts = pulumi.ResourceOptions(depends_on = [ inventory ]) + ) # Outputs -pulumi.export("ip", vm.ipv4_addresses[1][0]) +pulumi.export("ipv4_addresses", vm.ipv4_addresses) diff --git a/pvm-ansible/additional_config.py b/pvm-ansible/additional_config.py new file mode 100644 index 0000000..3114c89 --- /dev/null +++ b/pvm-ansible/additional_config.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +sshkeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFioHkaV1NhX6NCqsJakJw8EVBOcDHm1MEbpY499CPtG syntaxerrormmm@fisso", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILu91hBh8pNRt4eE1pug0Y4jCHZDCcMJ+vj3CiF5EQHV syntaxerrormmm@syntaxxps", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILH5q/ObtC4VhNT88gebezP/svpvCoQLoZCh4DvUn4xq syntaxerrormmm@taz", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGdTHkPCPUhvrcGgU9M6/BaEeirStM/kBnFxsLyXyelt syntaxerrormmm@kurotsuchi" +] + +# Ansible configuration +playbooks = [ + "deploy.yml" +] + +os_to_template = { + "tpl-ci-debian12": 808, + "debian12": 808, + "bookworm": 808 +} + diff --git a/pvm-ansible/input.yaml b/pvm-ansible/input.yaml deleted file mode 100644 index f2c7ab5..0000000 --- a/pvm-ansible/input.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -pve: - url: "https://pve.vavassori.lcl:8006" - insecure: true - username: root@pam - nodename: pve - storage: local-lvm - -# Basic VM configuration -vm: - name: virtual-machine - clone: 802 - sockets: 1 - cores: 1 - ram: 2048 - disksize: 20 - username: syntaxerrormmm - password: cicciopasticcio - sshkeys: - - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFioHkaV1NhX6NCqsJakJw8EVBOcDHm1MEbpY499CPtG syntaxerrormmm@fisso" - - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILu91hBh8pNRt4eE1pug0Y4jCHZDCcMJ+vj3CiF5EQHV syntaxerrormmm@syntaxxps" - - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILH5q/ObtC4VhNT88gebezP/svpvCoQLoZCh4DvUn4xq syntaxerrormmm@taz" - - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGdTHkPCPUhvrcGgU9M6/BaEeirStM/kBnFxsLyXyelt syntaxerrormmm@kurotsuchi" - -# Ansible configuration -ansible_playbook: deploy.yml diff --git a/pvm-ansible/pvewrapper.py b/pvm-ansible/pvewrapper.py new file mode 100644 index 0000000..0d57e52 --- /dev/null +++ b/pvm-ansible/pvewrapper.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from typing import List, Optional +from proxmoxer import ProxmoxAPI +import time + +class PveWrapper: + + def __init__(self, hostname: str, username: str, token_name: str, token_value: str, nodename: str, verify_ssl=False) -> None: + """Wrapper for Proxmox API query.""" + + self.conn = ProxmoxAPI( + hostname, + user=username, + token_name=token_name, + token_value=token_value, + verify_ssl=verify_ssl) + + self.instance = self.conn.nodes(nodename) + + def ipv4_addresses(self, vmid: str) -> List[str]: + """Returns the list of ipv4_addresses.""" + + netints = {} + while True: + try: + netints = self.instance.qemu(int(vmid)).agent.get("network-get-interfaces") + except: + time.sleep(10) + continue + else: + # When QEMU agent is running, return the array. + # All non-lo interfaces + filtered = [ x for x in netints['result'] if x['name'] != 'lo' ] + + retval = [] + for interface in filtered: + ipv4_addresses = [ x['ip-address'] for x in interface['ip-addresses'] if x['ip-address-type'] == 'ipv4' ] + # We assume a new machine has just been spawned, so it has just 1 ipv4 address. + ipv4_address = ipv4_addresses[0] + + retval.append({ "name": interface['name'], + "ipv4_address": ipv4_address }) + + return retval diff --git a/pvm-ansible/requirements.txt b/pvm-ansible/requirements.txt index e872837..aa9f1dd 100644 --- a/pvm-ansible/requirements.txt +++ b/pvm-ansible/requirements.txt @@ -2,3 +2,5 @@ pulumi>=3.0.0,<4.0.0 pulumi-proxmoxve>=2.0.0 pulumi-command>=0.4.1 dotmap>=1.3.30 +requests>=2.32.0 +proxmoxer>=2.0.0