Skip to content

Instantly share code, notes, and snippets.

@pclose
Created October 24, 2017 22:36
Show Gist options
  • Select an option

  • Save pclose/68aa6a1424e3c923f79319f4fe44113a to your computer and use it in GitHub Desktop.

Select an option

Save pclose/68aa6a1424e3c923f79319f4fe44113a to your computer and use it in GitHub Desktop.
#!/usr/bin/python
#
# cp_virsh.py -pete 2016-03-15
#
#
'''
usage: cp-virsh.py [-h] [--remove] [--quiet] [--password PASSWORD]
[--memory MEMORY] [--cpus CPUS] [--add-disk]
[--disk-size DISK_SIZE] [--remove-vol]
domain
Provisions new libvirt VMs based on an existing domain/image. This probably
only works when run with python 2.7 on linux
positional arguments:
domain name of existing domain
optional arguments:
-h, --help show this help message and exit
--remove, -r removes the domain and its disk image
--quiet, -q run quitely
--password PASSWORD, -p PASSWORD
root password on guest
--memory MEMORY, -m MEMORY
memory in gigabits
--cpus CPUS, -c CPUS number of virtual CPUs
--add-disk adds a storage device to a domain
--disk-size DISK_SIZE
size in GB of disk for --add-disk
--remove-vol clean up the extra storage matching the domain pattern
'''
import libvirt
import pexpect
import subprocess
import sys
import re
import argparse
import os
import time
from xml.dom import minidom
TEMP_XML = "/tmp/temp.xml"
DISK_TEMPLATE = "/var/lib/libvirt/images/{}.qcow2"
NAME_PATTERN = re.compile("([6a-z]*)(\d*)")
STORAGE_PATTERN = re.compile("([6a-z]*)(\d*?)-(\d*?)\.qcow2")
STORAGE_POOL = "default"
HOSTNAME_SCRIPT = "/root/chg_hostname.sh"
HOST_BRIDGE = 'br0'
GUEST_USERNAME = 'root'
BOOT_DELAY = 20
CONSOLE_DELAY = 20
SHELL_PS_MATCH = '%s@%s'
MEM_PATTERN1 = '/<memory/ s/.*/<memory unit="KiB">{}<\/memory>/'
MEM_PATTERN2 = '/<currentMemory/ s/.*/<currentMemory unit="KiB">{}<\/currentMemory>/'
CPU_PATTERN1 = '/<vcpu / s/.*/<vcpu placement="static">{}<\/vcpu>/'
MIN_MEM = 1
MAX_MEM = 12
MIN_CPU = 1
MAX_CPU = 10
VOL_XML = """
<volume type='file'>
<name>{name}.qcow2</name>
<key>{path}</key>
<source>
</source>
<capacity unit='G'>{size}</capacity>
<allocation unit='bytes'>0</allocation>
<target>
<path>{path}</path>
<format type='qcow2'/>
<permissions>
<mode>0600</mode>
<owner>108</owner>
<group>116</group>
</permissions>
<compat>1.1</compat>
<features>
<lazy_refcounts/>
</features>
</target>
</volume>
"""
# Run a command on the system
def run_command(command, verify=False, quiet=False):
if verify:
print " ".join(command)
print "you're about to run this command... continue?"
c = raw_input("(y/n): ")
if c != "y": return False
if not quiet: print "EXEC: " + " ".join(command)
f = subprocess.call(command)
if f > 0:
print "EXEC FAILED!!!"
return False
return True
# Connect to the local libvirt daemon
def get_connection():
return libvirt.open("qemu:///system")
# Save all vm names in a list
def get_domains(conn):
return [e.name() for e in conn.listAllDomains()]
# Figure out next hostname to use as new_name
def get_new_hostname(domains, template_name, name_pattern):
# Find template naming pattern
h = name_pattern.match(template_name).groups()[0]
# Get the number portion of the names
nums = []
for e in domains:
m = name_pattern.match(e)
if m and m.groups()[0] == h:
nums.append(int(m.groups()[1]))
if len(nums) < 1:
print >>sys.stderr, "FAIL: no existing domains found matching pattern "+template_name
raise Exception
#return None
# Increment hostname by 1 or find gap
nums.sort()
cur = nums[0] + 1
for e in nums[1:]:
if e != cur: break
cur += 1
return "{0}{1:02d}".format(h, cur)
# Take a copy of the template and save it to a temp file
def get_xml_doc(conn, template_name, temp_xml):
xml = minidom.parseString(conn.lookupByName(template_name).XMLDesc())
print >>open(temp_xml, "wb"), xml.toxml()
# Returns gigabit size in kibibit
def parse_mem(gigabit):
try:
if int(gigabit) < MIN_MEM or int(gigabit) > MAX_MEM:
raise Exception("incorrect memory size")
return int(pow(2, 20) * int(gigabit))
except Exception, e:
raise Exception("parse_mem FAIL: Must specify a digit between {} and {} (ERR: {})".format(MIN_MEM, MAX_MEM,e))
# Verifies CPU string is valid
def parse_cpu(cpus):
try:
if int(cpus) < MIN_CPU or int(cpus) > MAX_CPU:
raise Exception("incorrect cpu count")
return int(cpus)
except Exception, e:
raise Exception("parse_cpu FAIL: Must specify a digit between {} and {} (ERR: {})".format(MIN_CPU, MAX_CPU,e))
def modify_xml_doc(temp_xml, template_name, new_name, memory, cpus, quiet):
# Remove uuid
run_command(['sed', '-i', '/uuid/d', temp_xml], quiet=quiet)
# Change name
run_command(['sed', '-i', 's/%s/%s/' % (template_name, new_name), temp_xml], quiet=quiet)
# Remove interface
run_command(['sed', '-i', '/interface/,/interface/d', temp_xml], quiet=quiet)
# Update memory
run_command(['sed', '-i', MEM_PATTERN1.format(memory), temp_xml], quiet=quiet)
run_command(['sed', '-i', MEM_PATTERN2.format(memory), temp_xml], quiet=quiet)
# Update CPUs
run_command(['sed', '-i', CPU_PATTERN1.format(cpus), temp_xml], quiet=quiet)
def create_vm(temp_xml, disk_template, new_name, quiet):
# Clone disk
run_command(['virsh', 'vol-clone', disk_template, '%s.qcow2' % new_name], quiet=quiet)
# Create the VM
run_command(['virsh', 'define', '%s' % temp_xml], quiet=quiet)
def start_vm(domain, quiet):
run_command(['virsh', 'start', domain], quiet=quiet)
def reboot_vm(domain, quiet):
run_command(['virsh', 'reboot', domain], quiet=quiet)
def add_interface(domain, quiet):
run_command(['virsh', 'attach-interface', domain, 'bridge', HOST_BRIDGE, '--persistent'], quiet=quiet)
# Run the chg_hostname script on the new VM
def run_chg_hostname(new_hostname, template_name, username, password, quiet=False):
if not quiet: print "Connecting to %s's console..." % new_hostname
try:
#child = pexpect.spawn('virsh console %s' % new_hostname, logfile=open("test.log", "wb"))
child = pexpect.spawn('virsh console %s' % new_hostname)
time.sleep(CONSOLE_DELAY)
child.sendline()
match = '%s login:' % template_name
child.expect(match)
child.sendline(username)
match = 'Password: '
child.expect(match)
child.sendline(password)
match = SHELL_PS_MATCH % (username, template_name)
child.expect(match)
child.sendline('sh "%s" "%s"' % (HOSTNAME_SCRIPT, new_hostname))
match = SHELL_PS_MATCH % (username, template_name)
child.expect(match)
child.sendline('logout')
child.sendcontrol(']')
except pexpect.TIMEOUT, e:
print >>sys.stderr, "ERROR!"
print >>sys.stderr, e.get_trace()
print >>sys.stderr, "Timeout on console session... here is what we see:"
print >>sys.stderr, child.buffer
print >>sys.stderr, "We were expecting:"
print >>sys.stderr, match
child.sendcontrol(']')
child.terminate()
# Adds a volume and attaches it to domain
def add_disk(domain, size, quiet):
conn = get_connection()
pool = conn.storagePoolLookupByName(STORAGE_POOL)
vol_num = get_vol_name(domain, pool, STORAGE_PATTERN)
vol_alpha = chr(vol_num + ord("a"))
vol_name = "{}-{:02d}".format(domain, vol_num)
vol_target = "vd{}".format(vol_alpha)
vol_path = DISK_TEMPLATE.format(vol_name)
vol_xml = VOL_XML.format(
name=vol_name,
size=size,
path=vol_path)
print "Creating new storage volume here: {}".format(vol_path)
pool.createXML(vol_xml, 0)
run_command(["virsh",
"attach-disk",
domain,
vol_path,
vol_target,
"--targetbus",
"virtio",
"--driver",
"qemu",
"--subdriver",
"qcow2",
"--live",#"--config"],
"--persistent"],
verify=not quiet, quiet=quiet)
# Gets the next disk name for a domain
def get_vol_name(domain, pool, name_pattern):
vol_list = [e for e in pool.listVolumes()]
nums = []
for e in vol_list:
m = name_pattern.match(e)
if m and m.groups()[0]+m.groups()[1] == domain:
nums.append(int(m.groups()[2]))
if len(nums) == 0: cur = 1
else:
nums.sort()
cur = 1
for e in nums:
if e != cur: break
cur += 1
return cur
# Returns a list of storage volumes that match name_pattern
def find_vol_list(domain, pool, name_pattern):
vol_list = [e for e in pool.listVolumes()]
result = []
for e in vol_list:
m = name_pattern.match(e)
if m and m.groups()[0]+m.groups()[1] == domain:
result.append(e)
return result
def remove_domain(domain, quiet):
disk_image = DISK_TEMPLATE.format(domain)
run_command(["virsh", "shutdown", domain], verify=not quiet, quiet=quiet)
run_command(["virsh", "undefine", domain], verify=not quiet, quiet=quiet)
run_command(["virsh", "vol-delete", "--pool", STORAGE_POOL,
os.path.basename(disk_image)], verify=not quiet, quiet=quiet)
remove_vol(domain, quiet)
def remove_vol(domain, quiet):
conn = get_connection()
pool = conn.storagePoolLookupByName(STORAGE_POOL)
for e in find_vol_list(domain, pool, STORAGE_PATTERN):
vol = DISK_TEMPLATE.replace(".qcow2","").format(e)
run_command(["virsh", "vol-delete", "--pool", STORAGE_POOL, vol],
verify=not quiet, quiet=quiet)
def create_domain(domain, password, memory, cpus, quiet):
disk_image = DISK_TEMPLATE .format(domain)
conn = get_connection ()
domains = get_domains (conn)
new_name = get_new_hostname (domains, domain, NAME_PATTERN)
if not new_name: return
try:
memory = parse_mem(memory)
cpus = parse_cpu(cpus)
except Exception, e:
print e
return
get_xml_doc(conn, domain, TEMP_XML)
conn.close()
modify_xml_doc(TEMP_XML, domain, new_name, memory, cpus, quiet)
create_vm(TEMP_XML, disk_image, new_name, quiet)
start_vm(new_name, quiet)
time.sleep(BOOT_DELAY)
run_chg_hostname(new_name, domain, GUEST_USERNAME, password)
reboot_vm(new_name, quiet)
add_interface(new_name, quiet)
print "Finished trying to create "+ new_name
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Provisions new libvirt VMs '+
'based on an existing domain/image. This probably only works when run '+
'with python 2.7 on linux')
parser.add_argument("domain", help="name of existing domain")
parser.add_argument("--remove", "-r", action="store_true", help="removes "+
"the domain and its disk image")
parser.add_argument("--quiet", "-q", default=False, action="store_true", help="run quitely")
parser.add_argument("--password", '-p', help="root password on guest")
parser.add_argument("--memory", '-m', default=1, help="memory in gigabits")
parser.add_argument("--cpus", '-c', default=1, help="number of virtual CPUs")
parser.add_argument("--add-disk", default=False, action="store_true", help="adds a storage device to a domain")
parser.add_argument("--disk-size", default="8", help="size in GB of disk for --add-disk")
parser.add_argument("--remove-vol", action="store_true", help="clean up the extra storage matching the domain pattern")
args = parser.parse_args()
if args.remove:
remove_domain(args.domain, args.quiet)
elif args.add_disk:
add_disk(args.domain, args.disk_size, args.quiet)
elif args.remove_vol:
remove_vol(args.domain, args.quiet)
else:
if not args.password:
import getpass
args.password = getpass.getpass('root password on %s:' % args.domain)
create_domain(args.domain, args.password, args.memory, args.cpus, args.quiet)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment