initial commit
This commit is contained in:
commit
c544257bed
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
config.yaml
|
||||
*/__pycache__
|
||||
50
autonode/cobbler.py
Normal file
50
autonode/cobbler.py
Normal 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
152
autonode/proxmox.py
Normal 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
28
config.sample.yaml
Normal 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
74
createvm.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user