#! /usr/bin/python
#  -*- mode: python; -*-
#==============================================================================
# Copyright (C) 2013 Canonical Inc., Stefan Bader <stefan.bader@canonical.com>
#==============================================================================

"""Conversion tool for domain configurations in sxp format.
   Provides a way to convert xend managed domains into xm style config
   file which the xl command can handle.

   Run:
   xen-sxp2xm <config.sxp> [<output directory>]

   Reads the config.sxp and either writes the xm configuration to stdout
   or creates a <domain name>.cfg in the given directory if that file does
   not yet exist.

   WARNING: The conversion is not perfect. Manual inspection of the result
   is highly recommended.
"""
import sys, os.path
import types
import tempfile

sys.path.insert(1, sys.path[0] + '/../lib/python')

from xen.xend import XendDomain
from xen.xend import XendConfig


#
# Pop a key off the directory (if it exists) and if that is not the
# default setting, emit the data using newkey as the new key name.
#
def show_cfg_nondefault(cfg, key, newkey, out=sys.stdout):
	value = None
	if cfg.has_key(key):
		defaults = cfg._defaults()
		if defaults.has_key(key):
			if defaults[key] != cfg[key]:
				value = cfg[key]
		else:
			value = cfg[key]
		cfg.pop(key)

	if value:
		if not isinstance(value, (types.IntType, types.FloatType)):
			value = "'%s'" % value
		out.write("%s=%s\n" % (newkey, value))

def get_vif_string(devcfg):
	vifstr = ""
	sep = ""
	for key in devcfg.iterkeys():
		if key == "uuid":
			continue
		elif key == "backend":
			val = devcfg[key]
			if val != "0":
				diskstr += "%sbackend=%s" % (sep, val)
		else:
			vifstr += "%s%s=%s" % (sep, key, devcfg[key])
		sep = ","
	return vifstr

def get_disk_string(devcfg):
	diskstr = ""
	sep = ""
	target=""
	for key in devcfg.iterkeys():
		if key == "uuid":
			continue
		elif key == "bootable":
			continue
		elif key == "driver":
			continue
		elif key == "VDI":
			continue
		elif key == "protocol":
			continue
		elif key == "backend":
			val = devcfg[key]
			if val != "0":
				diskstr += "%sbackend=%s" % (sep, val)
		elif key == "dev":
			val = devcfg[key].split(":", 1)
			diskstr += "%svdev=%s" % (sep, val[0])
			if len(val) > 1 and val[1] == "cdrom":
				diskstr += ",devtype=cdrom"
		elif key == "mode":
			val = devcfg[key]
			if val == "w":
				val = "rw"
			elif val == "r":
				val = "ro"
			diskstr += "%saccess=%s" % (sep, val)
		elif key == "uname":
			val = devcfg[key]
			target = "target=%s" % val.split(":")[1]
		else:	
			diskstr += "%s%s=%s" % (sep, key, devcfg[key])
		sep = ","
	if target != "":
		diskstr += "%s%s" % (sep, target)
	return diskstr

def get_vfb_string(devcfg):
	vfbstr = ""
	sep = ""
	for key in devcfg.iterkeys():
		if key == "uuid":
			continue
		if key == "other_config":
			# other = devcfg[key]
			continue
		if key == "keymap":
			vfbstr += "%s%s=\"%s\"" % (sep, key, devcfg[key])
		else:
			vfbstr += "%s%s=%s" % (sep, key, devcfg[key])
		sep = ","
	return vfbstr

def show_dev_list(name, devlist, out=sys.stdout):
	if len(devlist) > 0:
		s = "%s=[" % name
		sep = ""
		for devstr in devlist:
			s = s + "%s '%s'" % (sep, devstr)
			sep = ","
		s = s + " ]\n"
		out.write(s)

def show_cfg_devices(devices, out=sys.stdout):
	vifs = []
	vfbs = []
	disks = []
	vtpms = []
	for uuid in devices.iterkeys():
		(devclass, devcfg) = devices[uuid]
		if devclass == "vif":
			vifs.append(get_vif_string(devcfg))
		elif devclass == "vbd":
			disks.append(get_disk_string(devcfg))
		elif devclass == "vfb":
			vfbs.append(get_vfb_string(devcfg))
		else:
			out.write("# WARNING: Unhandled device: %s\n" \
				  % devclass)
			for key in devcfg.iterkeys():
				out.write("# - %s=%s\n" % (key, devcfg[key]))
	show_dev_list("vfb", vfbs, out)
	show_dev_list("vif", vifs, out)
	show_dev_list("disk", disks, out)
	show_dev_list("vtpm", vtpms, out)

def show_cfg_xm(cfg, out=sys.stdout):
	hvm = False
	if cfg['platform'].has_key('loader') and \
	   "hvmloader" in cfg['platform']['loader']:
		hvm = True

	if hvm:
		out.write("#\n# Autogenerated HVM configuration\n")
	else:
		out.write("#\n# Autogenerated PV configuration\n")
	val = cfg.pop("description")
	if val != "":
		out.write("#\n#\n", val)
	val = cfg.pop("Description")
	if val != "":
		out.write("#\n#\n", val)
	out.write("#\n")
	if hvm:
		out.write("builder='hvm'\n")

	show_cfg_nondefault(cfg, "name_label", "name", out)
	show_cfg_nondefault(cfg, "uuid", "uuid", out)
	#
	# CPU options. Looks like the vcpu_avail bitmap maps best to
	# vpcus and VCPUs_max 
	#
	if cfg.has_key('vcpu_avail'):
		val = int(cfg.pop('vcpu_avail'))
		n   = 0
		while val:
			if val & 1:
				n += 1
			val /= 2
		out.write("vcpus=%i\n" % n)
		out.write("maxvcpus=%i\n" % cfg.pop('VCPUs_max'))
	else:
		val = int(cfg.pop('VCPUs_max', 1))
		out.write("vcpus=%i\n" % val)
		out.write("maxvcpus=%i\n" % val)

	#
	# Optionally show the weight and cap VCPU option
	#
	val = cfg.pop('vcpus_params')
	n   = val.pop('weight', 256)
	if n != 256:
		out.write("cpu_weight=%i\n" % n)
	n   = val.pop('cap', 0)
	if n > 0:
		out.write("cap=%i\n" % n)

	show_cfg_nondefault(cfg, "pool_name", "pool", out)
	# Ignored
	cfg.pop("VCPUs_live")
	cfg.pop("VCPUs_at_startup")

	#
	# Memory allocation. There seem to be memory_static_(min|max) and
	# memory_dynamic_(min|max). It looks a bit like static min might be
	# always 0...
	#
	val = cfg.pop('memory_static_min', 0)
	if val:
		out.write("WARNING: memory_static_min != 0 was not expected!\n")
	minmem = cfg.pop('memory_dynamic_min', 0)
	val  = cfg.pop('memory_static_max', 0)
	val1 = cfg.pop('memory_dynamic_max', 0)
	if val != val1:
		out.write("# WARNING: dynamic/static max memory mismatch " \
			  "(%i/%i)\n" % (val1, val))
	maxmem = max(val, val1)
	if minmem != maxmem:
		out.write("memory=%i\n" % (minmem / 1024 / 1024))
		out.write("maxmem=%i\n" % (maxmem / 1024 / 1024))
	else:
		out.write("memory=%i\n" % (minmem / 1024 / 1024))
	val = cfg.pop("shadow_memory")
	if hvm and val > 0:
		out.write("shadow_memory=%i\n" % (val))
	

	show_cfg_nondefault(cfg, "actions_after_shutdown", "on_poweroff", out)
	show_cfg_nondefault(cfg, "actions_after_reboot", "on_reboot", out)
	show_cfg_nondefault(cfg, "actions_after_crash", "on_crash", out)
	# Ignored
	cfg.pop("actions_after_suspend")
	cfg.pop("on_xend_start")
	cfg.pop("on_xend_stop")

	#
	# Devices
	#
	devices = cfg.pop("devices")
	if devices:
		show_cfg_devices(devices, out)

	#
	# Platform
	#
	val = cfg.pop("platform")
	if val.pop("nomigrate", "0") != "0":
		out.write("nomigrate=1\n")
	if cfg.pop("is_control_domain", False):
		out.write("driver_domain=1\n")

	#
	# PV guest specific
	#
	val = cfg.pop("PV_kernel")
	if not hvm:
		out.write("kernel='%s'\n" % val)
	val = cfg.pop("PV_ramdisk")
	if not hvm and val != "":
		out.write("ramdisk='%s'\n" % val)
	val = cfg.pop("PV_bootloader")
	if not hvm and val != "":
		out.write("bootloader='%s'\n" % val)
	val = cfg.pop("PV_bootloader_args")
	if not hvm and val != "":
		out.write("bootloader_args='%s'\n" % val)
	val = cfg.pop("PV_args")
	if not hvm and val != "":
		out.write("extra='%s'\n" % val)

	#
	# HVM guest specific
	#
	val = cfg.pop("HVM_boot_params")
	val = val.pop("order", "c")
	if hvm:
		out.write("boot=%s\n" % val)
	# Ignore
	cfg.pop("HVM_boot_policy")

	#
	# Things just ignored without complaint
	#
	cfg.pop("vbd_refs")
	cfg.pop("vif_refs")
	cfg.pop("console_refs")
	cfg.pop("auto_power_on")
	cfg.pop("status")
	cfg.pop("use_tmp_kernel")
	cfg.pop("use_tmp_ramdisk")

	out.write("#\n# The following options were not converted:\n#\n")
	for k in cfg.iterkeys():
		out.write("# %s=%s\n" % (k, cfg[k]))


#
# Check for one argument (the filename)
#
if len(sys.argv) < 2:
	print "Usage: %s <sxp file>" % os.path.basename(sys.argv[0])
	sys.exit(1)
cfgname = sys.argv[1]

#
# If a second argument is given, make sure this is pointing to a directory.
#
outdir = None
if len(sys.argv) == 3:
	outdir = sys.argv[2]
	if not os.path.isdir(outdir):
		print "Error: %s is not a directory!" % outdir
		sys.exit(1)

#
# Special pre-processing as it turned out that the sanity check of
# XendConfig object does not like the device_model or loader path
# to be wrong.
# For device_model just filter out the line to make the resulting
# config go for the default.
# For the loader, strip the pathname
#
(tmpfd, tmpname) = tempfile.mkstemp()
try:
	cfgfile = open(cfgname)
except:
	print "Failed to open %s" % cfgname
	tmpfd.close()
	unlink(tmpname)
	sys.exit(1)

for line in cfgfile:
		if "(device_model" in line:
			continue
		if "(loader" in line:
			idx = line.index("(loader")
			val = os.path.basename(line[idx+8:-1])
			line = "%s%s" % (line[:idx+8], val)
			
		os.write(tmpfd, line)
cfgfile.close()
os.close(tmpfd)


#
# Let XenConfig parse the pre-processed sxp file. Note to self: the raise
# is there because otherwise its pretty much impossible to find out what
# went wrong if things go wrong.
#
try:
	cfg = XendConfig.XendConfig(filename = tmpname)
except Exception:
	print('Unable to open of parse sxp file: %s' % cfgfile)
	os.unlink(tmpname)
	raise

os.unlink(tmpname)

if cfg.pop("is_a_template", False):
	print "Skipping %s (template)" % cfgname
	sys.exit(2)

#
# If this should go into an output directory, check whether that file
# actually is there already.
#
outfile = sys.stdout
if outdir:
	if not cfg.has_key("name_label"):
		print "Skipping %s without name defined." % cfgname
		sys.exit(2)
	outname = os.path.join(outdir, cfg["name_label"] + ".cfg")
	if os.path.exists(outname):
		print "Config %s already exists." % outname
		sys.exit(3)
	try:
		outfile = open(outname, "w")
	except:
		print "Failed to create %s!" % outname
		sys.exit(1)

if outdir:
	print "Creating %s" % outname
show_cfg_xm(cfg, outfile)

sys.exit(0)

