#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
""" vws - script to control QEMU/KVM virtual workstations """
# pylint: disable=too-many-lines
from configparser import ConfigParser
from argparse import ArgumentParser, Namespace
import fcntl
import socket
import select
import errno
import re
import os
import os.path
import fnmatch
import shlex
import shutil
import sys
import time
import pwd
import grp
VERSION = 0.8
def find_vm(name):
""" Search and return VM directory """
search_path = [os.path.join(pwd.getpwuid(os.getuid()).pw_dir, "VWs"),
config.get("directories", "SharedVMs"),
config.get("directories", "AutostartVMs")]
for dirname in search_path:
if not os.access(dirname, os.X_OK):
continue
if (name in os.listdir(dirname) and
os.access(os.path.join(dirname, name, "start"), os.X_OK)):
return os.path.join(dirname, name)
raise ValueError("Machine %s not found." % name)
def connect_vm(vm_dir):
""" Connects to monitor of VM in vm_dir and returns connected socket"""
sock = socket.socket(socket.AF_UNIX)
monitor_path = os.path.join(vm_dir, "monitor")
if not os.access(monitor_path, os.W_OK):
return None
try:
sock.connect(monitor_path)
except IOError as ex:
if ex.errno == errno.ECONNREFUSED:
# virtal machine is not running
return None
raise ex
readfd, _, _ = select.select([sock], [], [], 0.1)
if sock in readfd:
_ = sock.recv(1024)
return sock
def send_command(sock, command):
""" Sends monitor command to given socket and returns answer """
if sock is None:
raise RuntimeError("None socket is passed to send_command")
fcntl.flock(sock, fcntl.LOCK_EX)
try:
# There can be stray (qemu) prompt in the socket. Try to drain
# it
try:
sock.recv(64, socket.MSG_DONTWAIT)
except socket.error as ex:
if ex.errno != errno.EAGAIN and ex.errno != errno.EWOULDBLOCK:
raise ex
sock.send((command + "\n").encode("utf-8"))
answer = ""
while not answer.endswith("(qemu) "):
chunk = sock.recv(1024)
if chunk == b'':
raise IOError("Unexpected EOF from monitor")
answer += chunk.decode("utf-8")
finally:
fcntl.flock(sock, fcntl.LOCK_UN)
return answer
def spiceurl(sock):
""" Returns spice URI for given (as set of parsed args) VM """
output = send_command(sock, "info spice")
url = None
for line in output.split("\n"):
if url is not None:
continue
idx = line.find("address:")
if idx != -1:
url = line[idx+9:]
if url.startswith('*:'):
url = socket.getfqdn()+url[1:]
if url is None:
if output.rstrip().endswith('(qemu)'):
return spiceurl(sock)
print("ERROR parsing 'info spice' output:«%s»" % output, file=sys.stderr)
return None
return "spice://" + url.rstrip('\r')
def list_bridges():
""" Return list of bridge network interfaces present in the system """
lst = []
with os.popen(config.get('tools', 'bridge_list'), "r") as f:
for line in f:
idx = line.find('\t')
if idx <= 0:
continue
name = line[:idx]
if name == "bridge name":
continue
lst.append(name)
return lst
def parse_arp(iface):
"""
Returns map which maps mac addresses to IPs for specified interface"
"""
addr_map = {}
pipe = os.popen("%s -n -i %s" % (config.get("tools", "arp"), iface), "r")
for line in pipe:
data = line.split()
mac = data[2]
if mac == "HWAddress":
continue
if mac == iface:
# Foind line with (incomplete) entry
continue
addr_map[data[2]] = data[0]
return addr_map
def validate_size(size):
""" Checks if size argument has proper format """
return re.match('\\d+[KMG]', size) is not None
def get_drives(vm_dir):
""" Return list of drive files in the VW directory """
result = []
with open(vm_dir + "/start") as f:
for line in f:
if (re.match("\\s*-drive .*", line) and
line.find("media=disk") > -1):
match = re.search("file=([^,\\s]*)", line)
if match:
result.append(match.group(1))
return result
def snapshot_mode(sock):
""" Returns True if VM is running in snapshot mode """
answer = send_command(sock, "info block")
return re.search(": /tmp", answer) is not None
def read_netinfo(filename):
""" Reads network information from start script """
with open(filename, "r") as f:
for line in f:
match = re.search("-net nic,(?:\\S*,)?macaddr=(\\S+) -net ([^, ]+)", line)
if match:
f = {"mac":match.group(1)}
if match.group(2) == "user":
f["iface"] = "user"
elif match.group(2) == "bridge":
f["iface"] = re.search("br=(\\S+)", line).group(1)
else:
f["iface"] = "unknown"
return f
return {"iface":"unknown", "mac":"?", "ip":"?"}
def get_netinfo(sock):
""" Gets network information from the running VM """
answer = send_command(sock, "info network")
match = re.search("bridge\\.0:.*,br=(\\S+).*macaddr=(\\S+)", answer, re.S)
if match:
return {"iface":match.group(1), "mac":match.group(2)}
match = re.search("user.0:.*net=([^,]+).*\n.*macaddr=(\\S+)", answer)
if match:
return {"iface":"user", "ip":match.group(1),
"mac":match.group(2)}
print(answer, file=sys.stderr)
return {"iface":"unknown", "ip":"?", "mac":"?", "card":"?"}
#
# command implementation
#
def cmd_spiceuri(options):
""" vws spiceuri """
print(spiceurl(options.sock))
def fix_bridge_name(filename):
"""
Checks if bridge listed in network configuration
exists on this machine, and replaces it with default one
from config, if not so
"""
f = open(filename, "r+")
data = f.read()
idx0 = data.find("-net bridge,br=")
if idx0 != -1:
idx = data.find("=", idx0)
idx += 1
idx2 = data.find(" ", idx)
bridgename = data[idx:idx2]
if not bridgename in list_bridges():
net = config.get("create options", "net")
if net == "user":
data = data[:idx0] + "-net user" + data[idx2:]
else:
data = data[:idx] + net + data[idx2:]
f.seek(0)
f.write(data)
f.truncate()
f.close()
def check_for_snapshot(vmdir):
"""
Checks if there is snapshot of running vm which should be restored
Returns either id of this snapshot or None
"""
nxt = 0
with os.popen("qemu-img info \"%s\"" %
(os.path.join(vmdir, get_drives(vmdir)[0])), "r") as f:
for line in f:
if line == 'Snapshot list:\n':
nxt = 2
elif nxt == 2 and line.startswith('ID'):
nxt = 1
elif nxt == 1:
nxt = 0
snapshot_id = line[:line.index(' ')]
return snapshot_id
return None
def make_start_cmdline(options):
"""
Append options passed from vws commandline to start script
commandline
"""
arg = []
if options.cdrom:
arg.append("-cdrom")
arg.append(os.path.abspath(options.cdrom[0]))
if options.snapshot:
arg.append("-snapshot")
if options.args:
arg.append( "".join(options.args))
if options.password:
os.environ["SPICE_PASSWORD"] = options.password[0]
if arg:
return shlex.join(arg)
return ""
def cmd_start(options):
""" vws start """
if not "DISPLAY" in os.environ:
# If cannot start GUI just don't do it.
options.gui = False
if options.stopped:
arg = make_start_cmdline(options)
cwd = os.getcwd()
os.chdir(options.dir)
# Check for snapshot
snapshot_id = check_for_snapshot(options.dir)
if snapshot_id is not None:
arg = arg + " " + shlex.join(["-loadvm", snapshot_id])
# Check for correct brige name
try:
os.stat("monitor")
except FileNotFoundError:
# We cannot find monitor socket. So this machine might be
# never run on this host
fix_bridge_name("start")
os.system("./start%s" % arg)
os.chdir(cwd)
time.sleep(2)
options.sock = connect_vm(options.dir)
if options.sock is None:
print("VM start failed", file=sys.stderr)
sys.exit(1)
if snapshot_id:
send_command(options.sock, "delvm " + snapshot_id)
else:
if options.snapshot or options.args or options.password:
print("Cannot change qemu options. " +
"VM is already running", file=sys.stderr)
if options.cdrom:
options.file = options.cdrom[0]
options.id = None
cmd_cdrom(options)
uri = spiceurl(options.sock)
if options.gui:
os.system((config.get('tools', 'viewer') + "&") % uri)
elif not options.stopped:
print("VM already running use uri %s" % uri, file=sys.stderr)
def cmd_stop(options):
""" vws stop """
if options.hard or snapshot_mode(options.sock):
try:
send_command(options.sock, 'quit')
except IOError as ex:
# Expect IOError here
if str(ex).find("EOF from monitor"):
print("monitor socket closed")
else:
raise ex
else:
print(send_command(options.sock, 'system_powerdown'))
def cmd_monitor(options):
""" vws monitor """
try:
print("(qemu) ", end="")
sys.stdout.flush()
while True:
readfd, dummy_w, dummy_x = select.select([sys.stdin, options.sock],
[], [])
if sys.stdin in readfd:
cmd = sys.stdin.readline()
# Check for eof
if cmd == "":
break
answer = send_command(options.sock, cmd.rstrip())
idx = answer.index('\n')
print(answer[idx+1:], end="")
eol = answer.endswith("\n")
sys.stdout.flush()
elif options.sock in readfd:
print("UNSOLICITED MESSAGE %" +
options.sock.recv(1000).decode("utf-8").rstrip())
eol = True
except KeyboardInterrupt:
if not eol:
print("")
eol = True
print("Keyboard interrupt")
if not eol:
print("")
def cmd_reset(options):
""" vws reset """
print(send_command(options.sock, 'system_reset'))
def cmd_save(options):
""" vws save """
answer = send_command(options.sock, 'savevm')
if re.search("Error", answer):
print(answer, file=sys.stderr)
sys.exit(1)
else:
options.hard = True
cmd_stop(options)
def cmd_cdrom(options):
""" vws cdrom """
if options.id is None:
# Search for devices which could be interpreted as CDROM
devlist = send_command(options.sock, "info block")
idx = devlist.find("info block")
if idx != -1:
devlist = devlist[devlist.find("\n", idx)+1:]
for dev in devlist.split("\r\n\r\n"):
if dev.find("\n Removable device: ") == -1:
continue
if dev.startswith("floppy"):
continue
dev_id = dev[:dev.find(":")]
idx = dev_id.find(" ")
if idx != -1:
dev_id = dev[:idx]
options.id = dev_id
break
if options.id is None:
print("No CDROM device found among:\n" + devlist, file=sys.stderr)
return 1
if options.file == "":
print("Please specify either --eject or iso image", file=sys.stderr)
return 1
if options.file is None:
answer = send_command(options.sock, "eject " + options.id)
else:
answer = send_command(options.sock, "change %s %s" %
(options.id, os.path.abspath(options.file)))
print(answer)
return 0
def find_usb(options, devices):
""" Search for pattern or address given in options in the
given list of devices.
List should be produced by get_host_devices() or
get_vm_devices()
"""
if hasattr("pattern", options):
for dev in devices:
if re.search(options.pattern, dev[1]):
options.address = dev[0]
break
elif not hasattr("address", options):
print("Address or search pattern for device " +
"is not specified", file=sys.stderr)
options.sock.close()
sys.exit(1)
return options.address
def get_host_devices():
""" Parses output of lsusb into list of tuples "address" "descr" """
# pylint: disable=global-statement, invalid-name
global config
f = os.popen(config.get('tools', "lsusb"), "r")
lst = []
for dev in f:
match = re.match('Bus (\\d+) Device (\\d+): (.*)$', dev)
if match:
if match.group(3).endswith("root hub"):
continue
lst.append((match.group(1) + "." + match.group(2), match.group(3)))
f.close()
return lst
def get_vm_devices(sock):
""" Parses output of info usb monitor command into list of devices"""
answer = send_command(sock, "info usb")
lst = []
for dev in answer.split("\n"):
match = re.match('Device (\\d+\\.\\d+), .*?, Product (.*)$', dev)
if match:
lst.append((match.group(1), match.group(2)))
return lst
def cmd_usb_insert(options):
""" vws usb insert """
address = find_usb(options, get_host_devices())
answer = send_command(options.sock, "usb_add host:%s" % address)
print(answer)
def cmd_usb_list(_):
""" vws usb list - just list all host devices """
for addr, descr in get_host_devices():
print("%s: %s" % (addr, descr))
def cmd_usb_attached(options):
""" vws usb attached - list devices assigned to given vm """
for dev in get_vm_devices(options.sock):
print("Address %s : %s" % (dev[0], dev[1]))
def cmd_usb_remove(options):
""" vws usb remove """
address = find_usb(options, get_vm_devices(options.sock))
answer = send_command(options.sock, "usb_del %s" % address)
print(answer)
def make_vm_listing(vmname, dirname, vmtype):
""" makes dict with vm properties for short index """
f = {"name":vmname, "type":vmtype}
sock = connect_vm(dirname)
if sock is None:
if check_for_snapshot(dirname):
f['state'] = "sleeping"
else:
f['state'] = 'stopped '
f.update({"uri":"-", "ip":"-"})
f.update(read_netinfo(os.path.join(dirname, "start")))
else:
uri = spiceurl(sock)
if uri is None:
f.update({"state":"problem ", "uri":"None", "ip":"-"})
f.update(read_netinfo(os.path.join(dirname, "start")))
else:
f["uri"] = uri[uri.rindex(":")+1:]
f.update(get_netinfo(sock))
sock.shutdown(socket.SHUT_RDWR)
sock.close()
f["state"] = "running "
return f
def add_ip_address(listing):
""" Adds IP addresses from ARP into VM listing """
bridges = set()
for vminfo in listing:
if "ip" not in vminfo:
bridges.add(vminfo["iface"])
arp_data = {}
for bridge in bridges:
arp_data.update(parse_arp(bridge))
for vminfo in listing:
if "mac" in vminfo and not "ip" in vminfo:
if vminfo["mac"] in arp_data:
vminfo["ip"] = arp_data[vminfo["mac"]]
else:
vminfo["ip"] = "-"
def all_vms(patterns=("*",)):
"""
Returns list of tuples vmname, vmtype, directory
for all vms
"""
search_path = [("private", os.path.join(pwd.getpwuid(os.getuid()).pw_dir,
"VWs")),
("shared", config.get("directories", "SharedVMs")),
("autostart", config.get("directories", "AutostartVMs"))]
vmlist = []
for (vmtype, dirname) in search_path:
if not os.access(dirname, os.X_OK):
continue
for vmname in os.listdir(dirname):
if not os.access(os.path.join(dirname, vmname, "start"), os.X_OK):
continue
matches = False
for pattern in patterns:
if fnmatch.fnmatch(vmname, pattern):
matches = True
break
if matches:
vmlist.append((vmname, vmtype, os.path.join(dirname, vmname)))
return vmlist
def cmd_list(options):
""" vws list """
count = 0
maxlen = 0
vms = []
for vmname, vmtype, dirname in all_vms(options.pattern):
count += 1
if maxlen < len(vmname):
maxlen = len(vmname)
if options.state:
vms.append(make_vm_listing(vmname, dirname, vmtype))
else:
vms.append({"name":vmname})
if options.state:
add_ip_address(vms)
for f in sorted(vms, key=lambda x: x["name"]):
if "state" in f:
f["name"] = f["name"].ljust(maxlen)
print(("%(name)s %(state)s %(type)-9s %(uri)-4s %(iface)-5s " +
"%(mac)s %(ip)s ") % f)
else:
print(f["name"])
if not count:
sys.exit(1)
def cmd_screenshot(options):
""" vws screenshot """
filename = os.path.abspath(options.filename)
print(send_command(options.sock, "screendump " + filename))
def cmd_record(options):
""" vws record """
filename = os.path.abspath(options.filename)
print(send_command(options.sock, "wavcapture " + filename))
def cmd_stoprecord(options):
""" vws stoprecord """
answer = send_command(options.sock, "info capture")
match = re.search('\\[(\\d+)\\]: ', answer)
if not match:
print("No sound recording in progress", file=sys.stderr)
sys.exit(1)
else:
print(send_command(options.sock, "stopcapture " + match.group(1)))
def cmd_sendkey(options):
""" vws sendkey """
for keyspec in options.keyspec:
if keyspec == " ":
keyspec = "spc"
print(send_command(options.sock, "sendkey " + keyspec))
def cmd_version(_):
""" vws cersion """
print(VERSION)
def cmd_snapshot(options):
""" vws snapshot - create snapshot """
if not options.stopped:
print("Cannot make snapshot of running VW", file=sys.stderr)
sys.exit(1)
drives = get_drives(options.dir)
os.chdir(options.dir)
newnames = {}
for i in drives:
name, ext = os.path.splitext(i)
newnames[i] = name + "." + options.snapname + ext
if os.path.exists(newnames[i]):
print("Snapshot %s already exists", options.snapname,
file=sys.stderr)
return 1
for i in drives:
os.rename(i, newnames[i])
os.system("qemu-img create -f qcow2 -b \"%s\" \"%s\"" %
(newnames[i], i))
os.chmod(i, 0o664)
return 0
def cmd_snapshots(options):
""" vws snapshots - list existing snapshots """
os.chdir(options.dir)
drives = get_drives(options.dir)
lst = []
info = {}
with os.popen("qemu-img info -U --backing-chain " + drives[0], "r") as f:
for line in f:
if line.find(": ") != -1:
var, val = line.strip().split(": ")
if val != "":
info[var] = val
elif line[0] == '\n':
lst.append(info)
info = {}
if info:
lst.append(info)
for snap in lst:
print("%-30s %+8s %+8s" % (snap["image"], snap["virtual size"],
snap["disk size"]))
def get_backing(drive):
""" find if partucular virtual drive has backing file and returns
its name
"""
with os.popen('qemu-img info "%s"' % drive, "r") as f:
for line in f:
match = re.match("backing.file: (.*)$", line)
if match:
return match.group(1)
return None
def cmd_revert(options):
""" reverts to last snapshot:
Removes latest snapshot images and creates new ones instead
number of snapshots in the stack is not changed by this command
"""
if not options.stopped:
print("Cannot revert running VW to snapshot", file=sys.stderr)
sys.exit(1)
os.chdir(options.dir)
for drive in get_drives(options.dir):
# Check if first has backing file
backing = get_backing(drive)
if not backing:
print("Drive %s has no snapshots" % drive, file=sys.stderr)
continue
# Unlink current image
os.unlink(drive)
# create new image with same backing file
os.system('qemu-img create -f qcow2 -b "%s" "%s"' % (backing, drive))
os.chmod(drive, 0o664)
def cmd_commit(options):
"""
Commits last snapshot changes into it's backing file
There would be one snapshot less for virtual machine
"""
if options.stopped:
#
# Stopped vm - last snapshot is commited into its backing file.
# Backing file is made current drive image
#
os.chdir(options.dir)
found = 0
for drive in get_drives(options.dir):
backing = get_backing(drive)
if backing is None:
continue
found = 1
os.system('qemu-img commit "%s"'%drive)
os.unlink(drive)
os.rename(backing, drive)
if not found:
print("No snapshots exist for this VM", file=sys.stderr)
sys.exit(1)
else:
if snapshot_mode(options.sock):
send_command(options.sock, "commit")
else:
print("VM is not running in snapshot mode", file=sys.stderr)
sys.exit(1)
def cmd_autostart(_):
"""
Starts all VMs which are in the autostart directory
"""
dirname = config.get("directories", "AutostartVMs")
userinfo = pwd.getpwnam(config.get("permissions", "autostart_user"))
if not userinfo:
print("User %s doesn't exists %s" %
config.get("permissions", "autostart_user"), file=sys.stderr)
sys.exit(1)
os.setgid(userinfo.pw_gid)
os.setuid(userinfo.pw_uid)
if not os.access(dirname, os.R_OK):
return
for name in os.listdir(dirname):
if not os.access(os.path.join(dirname, name, "start"), os.X_OK):
continue
machine_dir = os.path.join(dirname, name)
sock = connect_vm(machine_dir)
if sock:
# Machine already running
sock.shutdown(socket.SHUT_RDWR)
sock.close()
continue
start_opts = Namespace(machine=name, command='start',
dir=machine_dir, snapshot=False,
stopped=True, password=None,
args="", gui=False, cdrom=None)
try:
cmd_start(start_opts)
print(name+" ", end="")
finally:
# pylint: disable=no-member
if hasattr(start_opts, "sock") and start_opts.sock:
start_opts.sock.shutdown(socket.SHUT_RDWR)
print("")
def cmd_shutdown(options):
""" Search for all running machines and stops all of them """
dirlist = [config.get("directories", "AutostartVMs"),
config.get("directories", "SharedVms")]
if os.getresuid()[1] == 0:
dirlist += map(lambda x: os.path.expanduser("~"+x)+"/VWs",
grp.getgrnam(config.get("permissions",
"vm_group")).gr_mem)
else:
dirlist.append(os.path.expanduser("~")+"/VWs")
count = 1 #Fake positive values for there is not postcondition loop in the python
forced_finish = time.time() + options.timeout
while count > 0:
count = 0
if time.time() < forced_finish:
command = "system_powerdown"
else:
command = "quit"
for _, _, dirname in all_vms():
sock = connect_vm(dirname)
if not sock:
# not running
continue
count += 1
try:
send_command(sock, command)
except IOError:
#When hard_stopping,socket might be closed by way
sock.shutdown(socket.SHUT_RDWR)
sock.close()
if not options.wait:
return
if count > 0:
time.sleep(10)
TEMPLATE = """#!/bin/sh
# Get machine name from current directory name
NAME=$(basename $(pwd))
# if remote access is enabled, then there should be
# SPICE_PASSWORD=password
QEMU_AUDIO_DRV=spice
export QEMU_AUDIO_DRV
if [ -n "$SPICE_PASSWORD" ]; then
SPICE_AUTH="password=$SPICE_PASSWORD"
else
SPICE_AUTH="disable-ticketing,addr=127.0.0.1"
fi
SPICE_PORT=$(find_free_port 5900)
if [ "$1" = '-cdrom' ]; then
shift
CDROM=",file=$1"
shift
fi
#set umask to make machine group-accessable
umask 002
{qemubinary} -name $NAME {accel} \\
-m {memory} \\
{drive} \\
{cdrom}$CDROM \\
{rtc}{net} \\
{usb} \\
{sound} \\
-chardev socket,server,nowait,path=monitor,id=monitor \\
-mon chardev=monitor,mode=readline \\
-vga {vga} \\
-spice port=$SPICE_PORT,$SPICE_AUTH \\
-device virtio-serial -chardev spicevmc,id=vdagent,name=vdagent \\
-device virtserialport,chardev=vdagent,name=com.redhat.spice.0 \\
-device ich9-usb-ehci1,id=usb \\
-device ich9-usb-uhci1,masterbus=usb.0,firstport=0,multifunction=on \\
-chardev spicevmc,name=usbredir,id=usbredirchardev1 \\
-device usb-redir,chardev=usbredirchardev1,id=usbredirdev1 \\
-daemonize -pidfile pid {extraargs}
chgrp {group} monitor pid
chmod 0660 monitor pid
"""
BADSIZE = "Invalid size of %s specifed %s. Should have K, M or G suffix"
NOACCEL = "KVM acceleration disabled due to "
def cmd_create(parsed_args):
""" vws create - create new VM """
if not parsed_args.image and not validate_size(parsed_args.size):
raise ValueError(BADSIZE % ("disk", parsed_args.size))
if not validate_size(parsed_args.mem):
raise ValueError(BADSIZE % ("memory", parsed_args.size))
drivename = "drive0.qcow2"
options = {'qemubinary':'qemu-system-x86_64',
"accel":"-enable-kvm",
"memory":"1024M",
"vga":'qxl',
"drive":"-drive media=disk,index=0,if={interface},file={image}",
"cdrom":"-drive media=cdrom,index=2,if=ide",
"sound":"-soundhw hda",
"group":config.get("permissions", "vm_group"),
"usb":"-usb",
"rtc":"",
"extraargs":"${1:+\"$@\"}"}
macaddr = ":".join(['52'] + ["%02x" % x for x in os.urandom(5)])
if parsed_args.shared:
machinedir = os.path.join(config.get("directories", "SharedVMs"),
parsed_args.machine)
dirmode = 0o775
else:
machinedir = os.path.join(pwd.getpwuid(os.getuid()).pw_dir, "VWs",
parsed_args.machine)
dirmode = 0o775
if parsed_args.net != 'user':
bridges = list_bridges()
if not parsed_args.net in bridges:
raise ValueError("No such bridge %s. Available ones %s" %
(parsed_args.net, ", ".join(bridges)))
options["net"] = ("-net nic,macaddr=%s -net bridge,br=%s" %
(macaddr, parsed_args.net))
else:
options["net"] = "-net nic,macaddr=%s -net user" % (macaddr,)
options["qemubinary"] = 'qemu-system-' + parsed_args.arch
options["vga"] = parsed_args.vga
if not parsed_args.arch in ('i386', 'x86_64'):
print(NOACCEL + "target architecture", file=sys.stderr)
options.accel = ''
elif not os.access("/dev/kvm", os.W_OK):
print(NOACCEL + "unavailability on the host system", file=sys.stderr)
options.accel = ''
if not parsed_args.usb:
options["usb"] = ''
if not parsed_args.sound:
options["sound"] = ''
else:
options["sound"] = '-soundhw ' + parsed_args.sound
options["memory"] = parsed_args.mem
if parsed_args.localtime:
options["rtc"] = "-rtc base=localtime,clock=host \\\n"
if os.path.exists(machinedir):
if os.path.exists(os.path.join(machinedir, "start")):
exc = OSError("Virtual Worstation %s already exists" %
parsed_args.machine)
else:
exc = OSError("Cannot create VW directory, " +
"something on the way")
raise exc
# Creating directory for VM
os.makedirs(machinedir, dirmode)
parsed_args.dir = machinedir
if parsed_args.shared:
gid = grp.getgrnam(config.get("permissions", "vm_group")).gr_gid
uid = os.getuid()
os.chown(machinedir, uid, gid)
if config.getboolean("permissions", "setgid_vm"):
os.chmod(machinedir, 0o2775)
driveopts = {"interface":parsed_args.diskif, "image":drivename}
if parsed_args.install:
install_image = os.path.abspath(parsed_args.install)
if parsed_args.image:
# Copying image file
print("Copying %s to %s" %
(parsed_args.image, os.path.join(machinedir, drivename)),
file=sys.stderr)
os.system("qemu-img convert -O qcow2 -p %s %s" %
(parsed_args.image,
os.path.join(machinedir, drivename)))
os.chdir(machinedir)
else:
print("Creating new image file of %s" % parsed_args.size,
file=sys.stderr)
os.chdir(machinedir)
os.system("qemu-img create -f qcow2 %s %s" %
(drivename, parsed_args.size))
os.chmod(drivename, 0o664)
options["drive"] = options["drive"].format(**driveopts)
if hasattr(parsed_args, "debug") and parsed_args.debug:
print(repr(driveopts), repr(options["drive"]))
print(repr(options))
with open("start", "w") as script:
script.write(TEMPLATE.format(**options))
os.chmod('start', dirmode)
# If installation media is specified vws start for new vm
if parsed_args.install:
start_opts = Namespace(machine=parsed_args.machine, password=None,
command='start', cdrom=[install_image],
dir=machinedir, stopped=True, snapshot=False,
args="", gui=True)
try:
cmd_start(start_opts)
finally:
# pylint: disable=no-member
if hasattr(start_opts, "sock"):
start_opts.sock.shutdown(socket.SHUT_RDWR)
#
# Utility functions for arg parsing
#
def new_command(cmd_parser, name, **kwargs):
"""
Adds a subparser and adds a machine name argument to it
"""
parser = cmd_parser.add_parser(name, **kwargs)
parser.add_argument('machine', type=str, help='name of vm to operate on')
return parser
#
# prepare defaults for config
#
config = ConfigParser(interpolation=None) # pylint: disable=invalid-name
def config_defaults(conf):
""" Set default values for config options """
arch = os.uname()[4]
if re.match("i[3-9]86", arch):
arch = "i386"
elif arch.startswith("arm"):
arch = "arm"
conf.read_dict({'directories':{'SharedVMs':'/var/cache/vws/shared',
'AutoStartVMs':'/var/cache/vws/autostart'},
'create options':{'net':'user', 'size':'20G', 'mem':'1G',
'diskif':'virtio', 'sound':'hda',
'arch':arch, 'vga':'qxl'},
'tools':{'viewer':'remote-viewer %s',
'bridge_list':'/sbin/brctl show',
'lsusb':'lsusb',
'arp':'/usr/sbin/arp'},
'permissions':{'vm_group':'kvm',
'autostart_user':'root',
'setgid_vm':'yes'}})
def read_config(conf):
""" Read configration files """
if os.getuid() != 0:
home = pwd.getpwuid(os.getuid()).pw_dir
conf.read(['/etc/vws.conf',
os.path.join(home, ".config", "vws", "vws.conf"),
os.path.join(home, '.vwsrc')])
else:
conf.read(['/etc/vws.conf'])
def main():
""" Parse an arguments and execute everything """
global config
config_defaults(config)
read_config(config)
# Parse argument
args = ArgumentParser(description="Manage Virtual Workstations")
cmds = args.add_subparsers(dest='command', help="sub-command help")
p = cmds.add_parser("list", help="List existing VWs",
description="List existing VWs")
p.add_argument("-l", "--state", const=True, default=False, dest='state',
action='store_const', help='Show state of the machine')
p.add_argument('-u', "--usb", action='store_const', const=True,
default=False, dest='usb', help='Show connected USB devices')
p.add_argument("pattern", nargs='*', default='*', help="Name patterns")
p = cmds.add_parser("version", help="show vws version")
p = cmds.add_parser("autostart",
help="Autostart all VMs marked as autostartable")
p = cmds.add_parser("shutdown", help="shut down all running VMs")
p.add_argument('-w', "--wait",
help="wait until all machines would be shutdown",
action="store_const", const=True, default=False, dest="wait")
p.add_argument('-t', "--timeout", type=int, default=90,
help="how long to way for VMs shutdown before forcing it off")
# Power management
p = new_command(cmds, 'start', help='Start VW and connect to console',
description="Start VW if not running and connect " +
"to the console")
p.add_argument('--no-gui', dest='gui', action='store_const', const=False,
default=True, help='do not open console window')
p.add_argument('--cdrom', metavar='filename.iso', dest='cdrom', nargs=1,
help='connect specified iso image to VMs cdrom on start')
p.add_argument('--args', metavar='string', dest='args', nargs=1, default="",
help="Specify extra QEMU options")
p.add_argument("--snapshot", action='store_const', const=True, default=False,
help="Run without modifying disk image")
p.add_argument("--password", metavar='string', dest='password', nargs=1,
default=None, help="Set password for remote spice connection")
p = new_command(cmds, 'stop', help='Shut down virtual machine',
description="Terminate the VW, gracefully or ungracefully")
p.add_argument('--hard', help='Power off immediately', action='store_const',
dest='hard', const=True, default=False)
new_command(cmds, 'save', help='Save VW state and stop emulation',
description="Save VW state and stop emulation")
new_command(cmds, 'reset', help='Reboot a guest OS',
description="Reboot othe guuest OS")
# Removable devices management
p = new_command(cmds, 'cdrom', help='manage CDROM Drive',
description='"insert" an ISO image into emulated CD-ROM ' +
"or eject it")
p.add_argument('--id', type=str, default=None,
help='Identifier of CDROM drive if VM has more than one')
p.add_argument('file', nargs="?", default='',
help='ISO image or special file to connect to drive')
p.add_argument('--eject', dest='file', action='store_const', const=None)
usb = cmds.add_parser('usb', help='manage USB devices'
).add_subparsers(dest='subcommand',
help='manage USB devices')
p = new_command(usb, 'insert', help='attach device to the virtual machine')
p.add_argument('pattern', help='Pattern of device name to look up in lsusb')
p.add_argument('--address', type=str, dest='address', nargs=1,
help='exact address bus:device')
p = new_command(usb, 'remove', help='detach connected usb device')
p.add_argument('pattern', help='Pattern of device name to look up in lsusb')
p.add_argument('--address', type=str, dest='address', nargs=1,
help='exact address bus:device')
p = new_command(usb, 'attached', help='list devices attached to vm')
usb.add_parser('list', help='list devices available in the host system')
# Snapshot management
p = new_command(cmds, 'snapshot', help='Create new snapshot')
p.add_argument('snapname', help='snapshot name')
p = new_command(cmds, 'revert', help='Revert to snapshot')
p.add_argument('snapname', help='name of snapshot to revert to')
p = new_command(cmds, 'commit',
help='Commit snapshot changes into backing file')
p = new_command(cmds, 'snapshots', help='List existing snapshots')
# Screenshoits and recording
p = new_command(cmds, 'screenshot', help='take a screenshot',
description='Takes a screenshot',
epilog="Writes current screen contents of the virtual " +
"machine into PPM format file")
p.add_argument('filename', metavar='filename.ppm',
help='PPM image filename to write screenshot to')
p = new_command(cmds, 'record', help='Record audio output from VM')
p.add_argument('filename', help='wav file to record autdio to')
new_command(cmds, 'stoprecord', help='stop recording audio')
p = new_command(cmds, 'sendkey', help='Send a keystroke to VM',
description='Send a key combination into VM',
epilog='Each key combination should be passed as separate' +
'argument.\nAll non-alphanumeric keys should be passed by' +
'names, not characters\n(see table in the manual).'
)
p.add_argument('keyspec', help='key specification like ctrl-alt-delete',
nargs='+')
# Create new VM
p = new_command(cmds, 'create', help="Create new VW")
p.add_argument("--no-usb", help="Disable USB controller", action='store_const',
const=False, default=True, dest="usb")
p.add_argument("--size", metavar='size', help="Size of primary disk images",
dest="size", default=config.get('create options', 'size'))
p.add_argument("--arch", metavar='cputype', help="Emulated architecture",
dest="arch", default=config.get('create options', 'arch'))
p.add_argument("--no-sound", help="Disable sound card", action='store_const',
const=None, default=config.get('create options', 'sound'),
dest="sound")
p.add_argument('--localtime', action='store_const',
help="Show system clock as local time, not UTC to guest OS",
const=True, default=False, dest='localtime')
p.add_argument("--sound", metavar='cardtype', dest='sound',
help="Specify sound card type",
default=config.get('create options', 'sound'))
p.add_argument("--vga", metavar='cardtype',
help="specify video card type (cirrus,std,vmware,qxl) " +
"default " + config.get('create options', 'vga',),
dest="vga", default=config.get('create options', 'vga'))
p.add_argument("--net", help="Network - 'user' or bridge name",
dest='net', default=config.get('create options', 'net'))
p.add_argument("--mem", metavar='size', help="Size of memory",
dest="mem", default=config.get('create options', 'mem'))
p.add_argument("--diskif", metavar='interface-type',
help="Disk interface (virtio, scsi, ide)",
choices=['virtio', 'scsi', 'ide'], dest="diskif",
default=config.get('create options', 'diskif'))
p.add_argument('--shared', help='Create shared VW instead of private one',
action='store_const', const=True, dest='shared',
default=False)
p.add_argument('--image', metavar='filename', dest='image', default=None,
help='Existing disk image to import')
p.add_argument('--install', metavar='filename.iso', dest='install',
help='ISO image to install OS from', default=None)
# Miscellenia
p = new_command(cmds, 'monitor',
help='connect stdin/stdout to monitor of VM')
p = new_command(cmds,
'spiceuri', help='Output spice URI of machine')
parsed_args = args.parse_args(sys.argv[1:])
os.umask(0o002)
# Create command is totally different, so it is handled separately
if parsed_args.command == 'create':
try:
cmd_create(parsed_args)
except Exception as ex: # pylint: disable=broad-except
print(str(ex), file=sys.stderr)
if hasattr(parsed_args, "dir"):
shutil.rmtree(parsed_args.dir)
sys.exit(1)
sys.exit(0)
if parsed_args.command is None:
args.print_help()
sys.exit(0)
funcname = "cmd_" + parsed_args.command
if hasattr(parsed_args, "subcommand"):
funcname += "_" + parsed_args.subcommand
try:
func = globals()[funcname]
except KeyError:
print("Operation %s is not implemented" % funcname, file=sys.stderr)
sys.exit(3)
parsed_args.stopped = False
stopped_vm_commands = ['start', 'snapshot', 'revert', 'commit', 'snapshots']
if hasattr(parsed_args, 'machine'):
parsed_args.dir = find_vm(parsed_args.machine)
parsed_args.sock = connect_vm(parsed_args.dir)
if parsed_args.sock is None:
if not parsed_args.command in stopped_vm_commands:
print("Virtual machine %s is not running." % parsed_args.machine,
file=sys.stderr)
sys.exit(1)
else:
parsed_args.stopped = True
try:
func(parsed_args)
finally:
if hasattr(parsed_args, 'sock') and parsed_args.sock is not None:
parsed_args.sock.shutdown(socket.SHUT_RDWR)
parsed_args.sock.close()
main()