initial commit

This commit is contained in:
Ben Vincent 2024-06-10 10:57:29 +10:00
commit c544257bed
5 changed files with 306 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
config.yaml
*/__pycache__

50
autonode/cobbler.py Normal file
View File

@ -0,0 +1,50 @@
import xmlrpc.client
def create_system(mac_address, config, details, vmname):
"""
Create or edit a system in Cobbler with detailed configuration for network and management.
"""
try:
# Connect to the Cobbler XML-RPC server
cobbler = xmlrpc.client.ServerProxy(config['cobbler']['api_url'])
# Login and get a token
token = cobbler.login(config['cobbler']['user'], config['cobbler']['pass'])
# Create a new system and get its system_id
system_id = cobbler.new_system(token)
# Basic system modifications using the provided system data
cobbler.modify_system(system_id, 'name', vmname, token)
cobbler.modify_system(system_id, 'hostname', vmname, token)
# Setup interface
interface_modifications = {
f"macaddress-{details['interface']}": mac_address,
f"ipaddress-{details['interface']}": details['ip'],
f"dnsname-{details['interface']}": vmname,
f"static-{details['interface']}": "1",
f"netmask-{details['interface']}": details.get('netmask', config['defaults']['netmask']),
}
cobbler.modify_system(system_id, 'modify_interface', interface_modifications, token)
# Additional modifications
cobbler.modify_system(system_id, 'profile', details.get('profile', config['defaults']['profile']), token)
cobbler.modify_system(system_id, 'name_servers', details.get('name_servers', config['defaults']['name_servers']), token)
cobbler.modify_system(system_id, 'name_servers_search', details.get('name_servers_search', config['defaults']['name_servers_search']), token)
cobbler.modify_system(system_id, 'gateway', details['gw'], token)
cobbler.modify_system(system_id, 'mgmt_classes', details.get('role', config['defaults']['role']), token)
cobbler.modify_system(system_id, 'netboot_enabled', "1", token) # false is 0, true is 1
cobbler.modify_system(system_id, 'status', 'testing', token)
# Save and sync the changes
cobbler.save_system(system_id, token)
cobbler.sync(token)
except xmlrpc.client.ProtocolError as err:
print(f"A protocol error occurred: {err.errcode}, {err.errmsg}")
except xmlrpc.client.Fault as fault:
print(f"An XML-RPC fault occurred: {fault.faultCode}, {fault.faultString}")
except Exception as e:
print(f"An unexpected error occurred: {e}")

152
autonode/proxmox.py Normal file
View File

@ -0,0 +1,152 @@
from proxmoxer import ProxmoxAPI
import re
def connect_proxmox(config):
return ProxmoxAPI(config['proxmox']['host'],
user=config['proxmox']['user'],
password=config['proxmox']['pass'],
verify_ssl=config['proxmox']['verify_ssl'])
def find_next_vmid(proxmox):
"""
Finds the next available VMID, excluding template ranges.
"""
vmids = [int(vm['vmid']) for vm in proxmox.cluster.resources.get(type='vm')]
# Assuming template VMIDs are in the range 900-910
vmids = [vmid for vmid in vmids if not (900 <= vmid <= 910)]
if vmids:
return max(vmids) + 1
return 1
def find_node_for_vm(proxmox, details):
"""
Selects the Proxmox host with the most free memory that has at least the required memory available for a new VM.
"""
resources = proxmox.cluster.resources.get(type='node')
suitable_nodes = []
for resource in resources:
# Convert bytes to MB for comparison
free_memory_mb = (resource['maxmem'] - resource['mem']) // 1024 // 1024
if free_memory_mb >= details['mem']:
suitable_nodes.append({
'node': resource['node'],
'free_memory_mb': free_memory_mb
})
# Select the node with the most free memory
if suitable_nodes:
target_node = max(suitable_nodes, key=lambda x: x['free_memory_mb'])
return target_node['node']
return None
def generate_vm_name(proxmox, vmid, details):
"""
Generates a VM name based on the Proxmox cluster name, a given VMID, and a domain.
"""
# Fetch cluster status to get the cluster name
cluster_status = proxmox.cluster.status.get()
if not cluster_status:
raise ValueError("Failed to fetch cluster status or cluster status is empty")
# Extract the cluster name, assuming the first entry is the cluster information
cluster_name = cluster_status[0]['name'] if 'name' in cluster_status[0] else None
if not cluster_name:
raise ValueError("Cluster name is not available in the cluster status")
# Remove underscores and dashes from the cluster name
formatted_cluster_name = cluster_name.replace('_', '').replace('-', '')
# Generate the VM name
vm_name = f"{formatted_cluster_name}nxvm{vmid}.{details['domain']}"
return vm_name
def create_vm(proxmox, node, config, details, vmid, vmname):
"""
Create a VM in Proxmox using specified arguments merged with defaults from the config.
"""
# Retrieve disk_pool from details
disk_pool = details['pool']
# Basic VM parameters setup
params = {
'vmid': vmid,
'name': vmname,
'memory': details['mem'] * 1024, # Convert GB to MB for Proxmox
'balloon': details['mem'] * 1024,
'cores': details['vcpu'],
'net0': f'virtio,bridge={details["net0"]}',
'ostype': 'l26',
'scsihw': 'virtio-scsi-single',
'startup': 'order=20',
'acpi': 1,
'agent': 1,
'autostart': 0,
'onboot': 1,
}
# BIOS and machine type
if details['bios'] == 'uefi':
params['efidisk0'] = f"{disk_pool}:1"
params['machine'] = 'pc-q35-7.0'
else:
params['machine'] = 'pc-i440fx-7.0'
# Boot order
params['boot'] = 'nc'
params['bootdisk'] = 'virtio0'
# Create the VM without starting it
try:
proxmox.nodes(node).qemu.create(**params)
except Exception as e:
print(f"Failed to create VM: {e}")
# default disk settings
disk_settings = "aio=threads,backup=0,cache=none,discard=on"
# Disk 0 configuration
try:
if 'disk0' in details:
proxmox.nodes(node).qemu(vmid).config.get()
proxmox.nodes(node).qemu(vmid).config.post(**{
'vmid': vmid,
'virtio0': f"{disk_pool}:{details['disk0']},{disk_settings}"
})
except Exception as e:
print(f"Failed to configure disk 0: {e}")
# Disk 1 configuration
try:
if 'disk1' in details and details['disk1'] is not None:
proxmox.nodes(node).qemu(vmid).config.get()
proxmox.nodes(node).qemu(vmid).config.post(**{
'vmid': vmid,
'virtio1': f"{disk_pool}:{details['disk1']},{disk_settings}"
})
except Exception as e:
print(f"Failed to configure disk 1: {e}")
# Retrieve and return the MAC address of net0 interface
config = proxmox.nodes(node).qemu(vmid).config.get()
# Search for the MAC address in the string
mac_address_pattern = r"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})"
match = re.search(mac_address_pattern, config.get('net0'))
# Extract the MAC address if a match is found
mac_address = match.group(0) if match else None
print(f"Successfully created virtual-machine vmid: {vmid}, name: {params['name']}, mac: {mac_address}")
return mac_address
def start_vm(proxmox, node, vmid):
"""
Start a proxmox VM by VMID
"""
try:
# Start the VM
proxmox.nodes(node).qemu(vmid).status.start.post()
print(f"VM with VMID {vmid} on node '{node}' has been started.")
except Exception as e:
print(f"Failed to start VM: {e}")

28
config.sample.yaml Normal file
View File

@ -0,0 +1,28 @@
proxmox:
host: myproxmox-url.domain.tld:443
user: autonode@pve
pass: a-secure-password
node: a-node-name
verify_ssl: /etc/pki/tls/cert.pem
# verify_ssl: true | false
cobbler:
api_url: http://cobbler.domain.tld/cobbler_api
user: cobbler
pass: cobbler-password
defaults:
vcpu: 2
mem: 4
disk0: 32
disk1: 50
pool: vmdata
net0: prod1
bios: bios
role: roles::base
name_servers: ['198.18.13.12', '198.18.13.13']
name_servers_search: main.domain.tld
interface: eth0
netmask: 255.255.255.0
profile: almalinux8.9-kvm
domain: main.domain.tld

74
createvm.py Normal file
View File

@ -0,0 +1,74 @@
import argparse
import yaml
from autonode import proxmox as pve
from autonode import cobbler as cobbler
def load_config(config_path="config.yaml"):
"""
Load configuration from YAML file.
"""
with open(config_path, "r") as file:
return yaml.safe_load(file)
def parse_arguments():
"""
Parse command-line arguments.
"""
parser = argparse.ArgumentParser(description="Create a VM in Proxmox and register it in Cobbler.")
parser.add_argument("--vcpu", type=int, choices=range(1, 17), help="Number of virtual CPUs")
parser.add_argument("--mem", type=int, choices=range(1, 33), help="Memory in GB")
parser.add_argument("--disk0", default="32", help="Size of disk0 in GB")
parser.add_argument("--disk1", type=int, help="Size of disk1 in GB")
parser.add_argument("--pool", help="Disk pool name")
parser.add_argument("--net0", help="Bridge name for net0")
parser.add_argument("--ip", help="IPAddress name for net0")
parser.add_argument("--gw", help="Gateway for net0")
parser.add_argument("--interface", help="Interface name, e.g. eth0 or enp0s2")
parser.add_argument("--bios", choices=["bios", "uefi"], help="BIOS or UEFI")
parser.add_argument("--role", help="Management class in Cobbler")
parser.add_argument("--domain", help="Domain name for host")
return parser.parse_args()
def main():
config = load_config()
args = parse_arguments()
# define vm details by merging config and arguments
details = {
'mem': args.mem or config['defaults']['mem'],
'vcpu': args.vcpu or config['defaults']['vcpu'],
'disk0': args.disk0 or '32',
'disk1': args.disk1 or None,
'pool': args.pool or config['defaults']['pool'],
'net0': args.net0 or config['defaults']['net0'],
'ip': args.ip,
'gw': args.gw,
'interface': args.interface or config['defaults']['interface'],
'bios': args.bios or config['defaults']['bios'],
'domain': args.bios or config['defaults']['domain'],
}
# Initialise the proxmox connection
proxmox = pve.connect_proxmox(config)
# Find the next available VMID
vmid = pve.find_next_vmid(proxmox)
# Generate the VM name
vmname = pve.generate_vm_name(proxmox, vmid, details)
# Find the best host to create the VM on
node = pve.find_node_for_vm(proxmox, details)
# Create the VM and save the macaddress
mac_address = pve.create_vm(proxmox, node, config, details, vmid, vmname)
# Create system in Cobbler
cobbler.create_system(mac_address, config, details, vmname)
# Start the VM
pve.start_vm(proxmox, node, vmid)
if __name__ == "__main__":
main()