import pulumi import pulumi_libvirt as libvirt import yaml # function to convert GB to bytes for disk size stated in stack config def gb_to_bytes(gb: int) -> int: """Converts gigabytes to bytes for resource capacity.""" return int(gb) * 1024 * 1024 * 1024 config = pulumi.Config("kvm") ssh_user = config.get("ssh_user") or "root" base_image_path = config.require("base_image_path") nodes = config.require_object("nodes") secret_hash = config.require_secret("password_hash") # Load user data once outside the loop try: with open(config.require("user_data_path"), "r") as f: user_data = pulumi.Output.format(f.read(), password_hash=secret_hash) except Exception as e: # Handle case where file path is wrong or secret fails to resolve print(f"Warning: Could not process user_data. Skipping node deployment. Error: {e}") # Dictionary to cache providers per target host providers = {} for node in nodes: name = node["name"] target_host = node["host"] bridge_interface = node["bridge"] disk_gb = node["disk_gb"] memory_mb = node["memory_mb"] vcpu = node["vcpu"] # Get or cache providers in providers dictionary if target_host not in providers: providers[target_host] = libvirt.Provider( f"prov-{target_host}", uri=f"qemu+sshcmd://{ssh_user}@{target_host}/system" ) host_provider = providers[target_host] meta_data_yaml = yaml.dump({"instance-id": f"i-{name}", "local-hostname": name}) # Initialize network payload in V1 format network_config_payload = { "version": 1, "config": [] # Start with an empty list of configurations (The list container) } # physical interface physical_interface = { "type": "physical", "name": "eth0", "subnets": [ { "type": "static", "address": node["static_ip"], "gateway": node["gateway"], } ] } network_config_payload["config"].append(physical_interface) # Append the nameservers to network_config_payload (solves missing nameserver issues) if "dns_servers" in node: nameserver_block = { "type": "nameserver", "address": [str(ip) for ip in node["dns_servers"]] # Create clean python list of strings } network_config_payload["config"].append(nameserver_block) if "dns_search" in node and node["dns_search"]: network_config_payload["config"][0]["subnets"][0]["dns_search"] = [str(s) for s in node["dns_search"]] # Create clean python list of strings # complete network config yaml network_config_yaml = yaml.dump(network_config_payload) # cloud image backing store base_volume = libvirt.Volume( f"base-vol-{name}", name=f"{name}-base", pool=pool_target, capacity=gb_to_bytes(10), target={ "format": {"type": "qcow2"} }, create={ "content": { "url": base_image_path } }, opts=pulumi.ResourceOptions(provider=host_provider) ) guest_volume = libvirt.Volume( f"{name}", name=f"{name}", pool=pool_target, capacity=gb_to_bytes(disk_gb), target={ "format": {"type": "qcow2"} }, backing_store={ "path": base_volume.id, "format": { "type": "qcow2" } }, opts=pulumi.ResourceOptions(provider=host_provider) ) cloud_init_disk = libvirt.CloudinitDisk( f"ci-disk-{name}", meta_data=meta_data_yaml, user_data=user_data, network_config=network_config_yaml, # Passes the fully constructed network payload opts=pulumi.ResourceOptions(provider=host_provider) ) cloud_init_volume = libvirt.Volume( f"ci-vol-{name}", pool=pool_target, target={ "format": {"type": "raw"} }, create={ "content": { "url": cloud_init_disk.path } }, opts=pulumi.ResourceOptions(provider=host_provider) ) guest = libvirt.Domain( f"dom-{name}", name=name, type="kvm", autostart=True, cpu={ "mode": "host-passthrough" }, memory=memory_mb, memory_unit="MiB", vcpu=vcpu, os={ "type": "hvm", "type_arch": "x86_64", "type_machine": "q35", "firmware": "efi", "boot_devices": [] }, features={ "acpi": True }, devices={ "disks": [ { "source": { "volume": { "pool": guest_volume.pool, "volume": guest_volume.name } }, "target": { "dev": "vda", "bus": "virtio" }, "driver": { "type": "qcow2" }, "boot": { "order": 1 } }, { "device": "cdrom", "source": { "volume": { "pool": cloud_init_volume.pool, "volume": cloud_init_volume.name } }, "target": { "dev": "sda", "bus": "sata" }, "driver": { "type": "raw" } } ], "interfaces": [ { "type": "bridge", "model": { "type": "virtio" }, "source": { "bridge": { "bridge": bridge_interface } }, "wait_for_ip": { "timeout": 300, "source": "lease" } } ], "graphics": [ { "type": "vnc", "vnc": { "autoport": True, "listen": "127.0.0.1" } } ] }, opts=pulumi.ResourceOptions(provider=host_provider) ) pulumi.export("deployed_node", guest.name)