virt-manager/virtconv/parsers/vmx.py

388 lines
12 KiB
Python

#
# Copyright 2008 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#
import virtconv.formats as formats
import virtconv.vmcfg as vmcfg
import virtconv.diskcfg as diskcfg
import virtconv.netdevcfg as netdevcfg
import sys
import re
import os
import logging
import shlex
_VMX_MAIN_TEMPLATE = """
#!/usr/bin/vmplayer
# Generated by %(progname)s
# http://virt-manager.org/
# This is a Workstation 5 or 5.5 config file and can be used with Player
config.version = "8"
virtualHW.version = "4"
guestOS = "other"
displayName = "%(vm_name)s"
annotation = "%(vm_description)s"
guestinfo.vmware.product.long = "%(vm_name)s"
guestinfo.vmware.product.url = "http://virt-manager.org/"
guestinfo.vmware.product.class = "virtual machine"
numvcpus = "%(vm_nr_vcpus)s"
memsize = "%(vm_memory)d"
MemAllowAutoScaleDown = "FALSE"
MemTrimRate = "-1"
uuid.action = "create"
tools.remindInstall = "TRUE"
hints.hideAll = "TRUE"
tools.syncTime = "TRUE"
serial0.present = "FALSE"
serial1.present = "FALSE"
parallel0.present = "FALSE"
logging = "TRUE"
log.fileName = "%(vm_name)s.log"
log.append = "TRUE"
log.keepOld = "3"
isolation.tools.hgfs.disable = "FALSE"
isolation.tools.dnd.disable = "FALSE"
isolation.tools.copy.enable = "TRUE"
isolation.tools.paste.enabled = "TRUE"
floppy0.present = "FALSE"
"""
_VMX_ETHERNET_TEMPLATE = """
ethernet%(dev)s.present = "TRUE"
ethernet%(dev)s.connectionType = "nat"
ethernet%(dev)s.addressType = "generated"
ethernet%(dev)s.generatedAddressOffset = "0"
ethernet%(dev)s.autoDetect = "TRUE"
"""
_VMX_IDE_TEMPLATE = """
# IDE disk
ide%(dev)s.present = "TRUE"
ide%(dev)s.fileName = "%(disk_filename)s"
ide%(dev)s.mode = "persistent"
ide%(dev)s.startConnected = "TRUE"
ide%(dev)s.writeThrough = "TRUE"
"""
class _VMXLine(object):
"""
Class tracking an individual line in a VMX/VMDK file
"""
def __init__(self, content):
self.content = content
self.pair = None
self.is_blank = False
self.is_comment = False
self.is_disk = False
self._parse()
def _parse(self):
line = self.content.strip()
if not line:
self.is_blank = True
elif line.startswith("#"):
self.is_comment = True
elif line.startswith("RW ") or line.startswith("RDONLY "):
self.is_disk = True
else:
# Expected that this will raise an error for unknown format
before_eq, after_eq = line.split("=", 1)
key = before_eq.strip().lower()
value = after_eq.strip().strip('"')
self.pair = (key, value)
def parse_disk_path(self):
# format:
# RW 16777216 VMFS "test-flat.vmdk"
# RDONLY 156296322 V2I "virtual-pc-diskformat.v2i"
content = self.content.split(" ", 3)[3]
if not content.startswith("\""):
raise ValueError("Path was not fourth entry in VMDK storage line")
return shlex.split(content, " ", 1)[0]
class _VMXFile(object):
"""
Class tracking a parsed VMX/VMDK format file
"""
def __init__(self, content):
self.content = content
self.lines = []
self._parse()
def _parse(self):
for line in self.content:
try:
lineobj = _VMXLine(line)
self.lines.append(lineobj)
except Exception, e:
raise Exception(_("Syntax error at line %d: %s\n%s") %
(len(self.lines) + 1, line.strip(), e))
def pairs(self):
ret = {}
for line in self.lines:
if line.pair:
ret[line.pair[0]] = line.pair[1]
return ret
def parse_vmdk(disk, filename):
"""
Parse a VMDK descriptor file
Reference: http://sanbarrow.com/vmdk-basics.html
"""
# Detect if passed file is a descriptor file
# Assume descriptor isn't larger than 10K
if not os.path.exists(filename):
logging.debug("VMDK file '%s' doesn't exist", filename)
return
if os.path.getsize(filename) > (10 * 1024):
logging.debug("VMDK file '%s' too big to be a descriptor", filename)
return
f = open(filename, "r")
content = f.readlines()
f.close()
try:
vmdkfile = _VMXFile(content)
except:
logging.exception("%s looked like a vmdk file, but parsing failed",
filename)
return
disklines = [l for l in vmdkfile.lines if l.is_disk]
if len(disklines) == 0:
raise RuntimeError(_("Didn't detect a storage line in the VMDK "
"descriptor file"))
if len(disklines) > 1:
raise RuntimeError(_("Don't know how to handle multistorage VMDK "
"descriptors"))
diskline = disklines[0]
newpath = diskline.parse_disk_path()
logging.debug("VMDK file parsed path %s->%s", disk.path, newpath)
disk.path = newpath
def parse_netdev_entry(vm, fullkey, value):
"""
Parse a particular key/value for a network. Throws ValueError.
"""
ignore, ignore, inst, key = re.split("^(ethernet)([0-9]+).", fullkey)
lvalue = value.lower()
if key == "present" and lvalue == "false":
return
if not vm.netdevs.get(inst):
vm.netdevs[inst] = netdevcfg.netdev(typ=netdevcfg.NETDEV_TYPE_UNKNOWN)
# "vlance", "vmxnet", "e1000"
if key == "virtualdev":
vm.netdevs[inst].driver = lvalue
if key == "addresstype" and lvalue == "generated":
vm.netdevs[inst].mac = "auto"
# we ignore .generatedAddress for auto mode
if key == "address":
vm.netdevs[inst].mac = lvalue
def parse_disk_entry(vm, fullkey, value):
"""
Parse a particular key/value for a disk. FIXME: this should be a
lot smarter.
"""
# skip bus values, e.g. 'scsi0.present = "TRUE"'
if re.match(r"^(scsi|ide)[0-9]+[^:]", fullkey):
return
ignore, bus, bus_nr, inst, key = re.split(r"^(scsi|ide)([0-9]+):([0-9]+)\.",
fullkey)
lvalue = value.lower()
if key == "present" and lvalue == "false":
return
# Does anyone else think it's scary that we're still doing things
# like this?
if bus == "ide":
inst = int(bus_nr) * 2 + (int(inst) % 2)
elif bus == "scsi":
inst = int(bus_nr) * 16 + (int(inst) % 16)
devid = (bus, inst)
if not vm.disks.get(devid):
vm.disks[devid] = diskcfg.disk(bus=bus,
typ=diskcfg.DISK_TYPE_DISK)
disk = vm.disks[devid]
if key == "devicetype":
if lvalue == "atapi-cdrom" or lvalue == "cdrom-raw":
disk.type = diskcfg.DISK_TYPE_CDROM
elif lvalue == "cdrom-image":
disk.type = diskcfg.DISK_TYPE_ISO
if key == "filename":
disk.path = value
disk.format = diskcfg.DISK_FORMAT_RAW
if lvalue.endswith(".vmdk"):
disk.format = diskcfg.DISK_FORMAT_VMDK
# See if the filename is actually a VMDK descriptor file
parse_vmdk(disk, disk.path)
class vmx_parser(formats.parser):
"""
Support for VMWare .vmx files. Note that documentation is
particularly sparse on this format, with pretty much the best
resource being http://sanbarrow.com/vmx.html
"""
name = "vmx"
suffix = ".vmx"
can_import = True
can_export = True
can_identify = True
@staticmethod
def identify_file(input_file):
"""
Return True if the given file is of this format.
"""
infile = open(input_file, "r")
content = infile.readlines()
infile.close()
for line in content:
# some .vmx files don't bother with the header
if (re.match(r'^config.version\s+=', line) or
re.match(r'^#!\s*/usr/bin/vm(ware|player)', line)):
return True
return False
@staticmethod
def import_file(input_file):
"""
Import a configuration file. Raises if the file couldn't be
opened, or parsing otherwise failed.
"""
vm = vmcfg.vm()
infile = open(input_file, "r")
contents = infile.readlines()
infile.close()
logging.debug("Importing VMX file:\n%s", "".join(contents))
vmxfile = _VMXFile(contents)
config = vmxfile.pairs()
if not config.get("displayname"):
raise ValueError(_("No displayName defined in '%s'") %
input_file)
vm.name = config.get("displayname")
vm.memory = config.get("memsize")
vm.description = config.get("annotation")
vm.nr_vcpus = config.get("numvcpus")
for key, value in config.items():
if key.startswith("scsi") or key.startswith("ide"):
parse_disk_entry(vm, key, value)
if key.startswith("ethernet"):
parse_netdev_entry(vm, key, value)
for devid, disk in vm.disks.iteritems():
if disk.type == diskcfg.DISK_TYPE_DISK:
continue
# vmx files often have dross left in path for CD entries
if (disk.path is None
or disk.path.lower() == "auto detect" or
not os.path.exists(disk.path)):
vm.disks[devid].path = None
vm.validate()
return vm
@staticmethod
def export(vm):
"""
Export a configuration file as a string.
@vm vm configuration instance
Raises ValueError if configuration is not suitable.
"""
vm.description = vm.description.strip()
vm.description = vm.description.replace("\n", "|")
vmx_out_template = []
vmx_dict = {
#"now": time.strftime("%Y-%m-%dT%H:%M:%S %Z", time.localtime()),
"progname": os.path.basename(sys.argv[0]),
"vm_name": vm.name,
"vm_description": vm.description or "None",
"vm_nr_vcpus" : vm.nr_vcpus,
"vm_memory": long(vm.memory)
}
vmx_out = _VMX_MAIN_TEMPLATE % vmx_dict
vmx_out_template.append(vmx_out)
disk_out_template = []
for devid, disk in sorted(vm.disks.items()):
bus, dev_nr = devid
if bus.lower() != "ide":
logging.debug("Disk bus '%s' not yet supported. Skipping.",
bus.lower())
continue
dev = "%d:%d" % (dev_nr / 2, dev_nr % 2)
disk_dict = {
"dev": dev,
"disk_filename" : disk.path
}
disk_out = _VMX_IDE_TEMPLATE % disk_dict
disk_out_template.append(disk_out)
eth_out_template = []
if len(vm.netdevs):
for devnum in vm.netdevs:
eth_dict = {
"dev" : devnum
}
eth_out = _VMX_ETHERNET_TEMPLATE % eth_dict
eth_out_template.append(eth_out)
return "".join(vmx_out_template + disk_out_template + eth_out_template)
formats.register_parser(vmx_parser)