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