Commit b5085c54 authored by Constantinos Venetsanopoulos's avatar Constantinos Venetsanopoulos

Merge pull request #18 from skalkoto/feature-virtio-installation

Feature VirtIO installation.

Automatically install or upgrade the VirtIO drivers on Windows images.
This resolves #7.
parents 5030fe91 c12f530c
......@@ -24,6 +24,7 @@ import textwrap
import StringIO
import json
import re
import time
from image_creator import __version__ as version
from image_creator.util import MD5, FatalError
......@@ -32,7 +33,7 @@ from image_creator.kamaki_wrapper import Kamaki, ClientError
from image_creator.help import get_help_file
from image_creator.dialog_util import SMALL_WIDTH, WIDTH, \
update_background_title, confirm_reset, confirm_exit, Reset, \
extract_image, add_cloud, edit_cloud, select_file
extract_image, add_cloud, edit_cloud, virtio_versions, update_sysprep_param
CONFIGURATION_TASKS = [
("Partition table manipulation", ["FixPartitionTable"],
......@@ -640,41 +641,6 @@ def exclude_tasks(session):
return True
def update_sysprep_param(session, name):
"""Modify the value of a sysprep parameter"""
d = session['dialog']
image = session['image']
param = image.os.sysprep_params[name]
while 1:
if param.type in ("file", "dir"):
title = "Please select a %s to use for the `%s' parameter" % \
(name, 'file' if param.type == 'file' else 'directory')
ftype = "br" if param.type == 'file' else 'd'
value = select_file(d, ftype=ftype, title=title)
if value is None:
return False
else:
(code, answer) = d.inputbox(
"Please provide a new value for configuration parameter: `%s'"
% name, width=WIDTH, init=str(param.value))
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return False
value = answer.strip()
if param.set_value(value) is False:
d.msgbox("Unable to update the value. Reason: %s" % param.error,
width=WIDTH)
param.error = None
continue
break
def sysprep_params(session):
"""Collect the needed sysprep parameters"""
d = session['dialog']
......@@ -684,9 +650,14 @@ def sysprep_params(session):
while 1:
choices = []
for name, param in image.os.sysprep_params.items():
# Don't show the hidden parameters
if param.hidden:
continue
value = str(param.value)
if len(value) == 0:
value = "<empty>"
value = "<not_set>"
choices.append((name, value))
if len(choices) == 0:
......@@ -718,6 +689,110 @@ def sysprep_params(session):
return True
def virtio(session):
"""Display the state of the VirtIO drivers in the media"""
d = session['dialog']
image = session['image']
assert hasattr(image.os, 'virtio_state')
assert hasattr(image.os, 'install_virtio_drivers')
default_item = image.os.virtio_state.keys()[0]
while 1:
choices = []
for name, details in virtio_versions(image.os.virtio_state).items():
choices.append((name, details))
(code, choice) = d.menu(
"In this menu you can see details about the installed VirtIO "
"drivers on the input media. Press <OK> to see more information "
"about a specific installed driver or <Update> to install one or "
"more new drivers.", height=16, width=WIDTH, choices=choices,
menu_height=len(choices), cancel="Back", title="VirtIO Drivers",
extra_button=1, extra_label="Update", default_item=default_item)
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return True
elif code == d.DIALOG_OK:
default_item = choice
# Create a string with the driver details and display it.
details = ""
for fname, driver in image.os.virtio_state[choice].items():
details += "%s\n%s\n" % (fname, "=" * len(fname))
name = ""
if 'DriverPackageDisplayName' in driver:
name = driver['DriverPackageDisplayName']
provider = ""
if 'Provider' in driver:
provider = driver['Provider']
date = ""
version = ""
if 'DriverVer' in driver:
version = driver['DriverVer'].split(',', 1)
date = version[0].strip()
version = version[1] if len(version) > 1 else ""
try:
date = time.strptime(
date, "%m/%d/%y").strftime('%d/%m/%Y', date)
except ValueError:
pass
dtype = ""
if 'DriverPackageType' in driver:
dtype = driver['DriverPackageType']
dclass = ""
if 'Class' in driver:
dclass = driver['Class']
details += "Name: %s\n" % name.strip('\'"')
details += "Provider: %s\n" % provider.strip('\'"')
details += "Date: %s\n" % date
details += "Version: %s\n" % version
details += "Type: %s\n" % dtype
details += "Class: %s\n\n" % dclass
if len(details):
d.scrollbox(details, width=WIDTH)
else: # Update button
title = "Please select a directory that hosts VirtIO drivers."
if not update_sysprep_param(session, "virtio", title=title):
continue
install_virtio_drivers(session)
return True
def install_virtio_drivers(session):
"""Installs new VirtIO drivers in the image"""
d = session['dialog']
image = session['image']
assert hasattr(image.os, 'install_virtio_drivers')
if d.yesno("Continue with the installation of the VirtIO drivers?",
width=SMALL_WIDTH, defaultno=1):
return False
title = "VirtIO Drivers Installation"
infobox = InfoBoxOutput(d, title)
try:
image.out.add(infobox)
try:
image.os.install_virtio_drivers()
infobox.finalize()
except FatalError as e:
d.msgbox("VirtIO Drivers Installation failed: %s" % e, title=title,
width=SMALL_WIDTH)
return False
finally:
image.out.remove(infobox)
finally:
infobox.cleanup()
return True
def sysprep(session):
"""Perform various system preparation tasks on the image"""
d = session['dialog']
......@@ -760,13 +835,18 @@ def sysprep(session):
(code, tags) = d.checklist(
"Please choose which system preparation tasks you would like to "
"run on the image. Press <Help> to see details about the system "
"preparation tasks.", title="Run system preparation tasks",
choices=choices, width=70, ok_label="Run", help_button=1)
"run on the image. Press <Params> to view or modify the "
"customization parameters or <Help> to see details about the "
"system preparation tasks.", title="Run system preparation tasks",
choices=choices, width=70, ok_label="Run", help_button=1,
extra_button=1, extra_label="Params")
tags = map(lambda x: x.strip('"'), tags) # Needed for OpenSUSE
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return False
elif code == d.DIALOG_EXTRA:
sysprep_params(session)
elif code == d.DIALOG_HELP:
d.scrollbox(sysprep_help, width=WIDTH)
elif code == d.DIALOG_OK:
......@@ -847,16 +927,20 @@ def shrink(session):
def customization_menu(session):
"""Show image customization menu"""
d = session['dialog']
image = session['image']
choices = [("Parameters", "View & Modify customization parameters"),
("Sysprep", "Run various image preparation tasks"),
("Shrink", "Shrink image"),
("Properties", "View & Modify image properties"),
("Exclude", "Exclude various deployment tasks from running")]
choices = []
if hasattr(image.os, "install_virtio_drivers"):
choices.append(("Virtio", "Install or update the VirtIO drivers"))
choices.extend(
[("Sysprep", "Run various image preparation tasks"),
("Shrink", "Shrink image"),
("Properties", "View & Modify image properties"),
("Exclude", "Exclude various deployment tasks from running")])
default_item = 0
actions = {"Parameters": sysprep_params,
actions = {"Virtio": virtio,
"Sysprep": sysprep,
"Shrink": shrink,
"Properties": modify_properties,
......
......@@ -336,4 +336,56 @@ def edit_cloud(session, name):
return True
def virtio_versions(virtio_state):
"""Returns the versions of the drivers defined by the virtio state"""
ret = {}
for name, infs in virtio_state.items():
driver_ver = [drv['DriverVer'].split(',', 1) if 'DriverVer' in drv
else [] for drv in infs.values()]
vers = [v[1] if len(v) > 1 else " " for v in driver_ver]
ret[name] = "<not found>" if len(infs) == 0 else ", ".join(vers)
return ret
def update_sysprep_param(session, name, title=None):
"""Modify the value of a sysprep parameter"""
d = session['dialog']
image = session['image']
param = image.os.sysprep_params[name]
while 1:
if param.type in ("file", "dir"):
if not title:
title = "Please select a %s to use for the `%s' parameter" % \
('file' if param.type == 'file' else 'directory', name)
ftype = "br" if param.type == 'file' else 'd'
value = select_file(d, ftype=ftype, title=title)
if value is None:
return False
else:
if not title:
title = "Please provide a new value for configuration " \
"parameter: `%s'" % name
(code, answer) = d.inputbox(
title, width=WIDTH, init=str(param.value))
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return False
value = answer.strip()
if param.set_value(value) is False:
d.msgbox("Unable to update the value. Reason: %s" % param.error,
width=WIDTH)
param.error = None
continue
break
return True
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
......@@ -27,11 +27,10 @@ from image_creator.kamaki_wrapper import Kamaki, ClientError
from image_creator.util import MD5, FatalError
from image_creator.output.cli import OutputWthProgress
from image_creator.dialog_util import extract_image, update_background_title, \
add_cloud, edit_cloud
add_cloud, edit_cloud, virtio_versions, update_sysprep_param
PAGE_WIDTH = 70
PAGE_HEIGHT = 10
SYSPREP_PARAM_MAXLEN = 20
PAGE_HEIGHT = 12
class WizardExit(Exception):
......@@ -128,6 +127,37 @@ class WizardPage(object):
raise NotImplementedError
class WizardInfoPage(WizardPage):
"""Represents a Wizard Page that just displays some user-defined
information.
"""
def __init__(self, name, display_name, text, body, **kargs):
super(WizardInfoPage, self).__init__(name, display_name, text, **kargs)
self.body = body
def run(self, session, title):
d = session['dialog']
w = session['wizard']
extra_button = 1 if self.extra else 0
text = "%s\n\n%s" % (self.text, self.body())
ret = d.yesno(text, width=PAGE_WIDTH, ok_label="Next",
cancel="Back", extra_button=extra_button, title=title,
extra_label=self.extra_label, height=PAGE_HEIGHT)
if ret in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return self.PREV
elif ret == d.DIALOG_EXTRA:
self.extra()
raise WizardReloadPage
# DIALOG_OK
w[self.name] = self.validate(None)
self.info = "%s: %s" % (self.display_name, self.display(w[self.name]))
return self.NEXT
class WizardPageWthChoices(WizardPage):
"""Represents a Wizard Page that allows the user to select something from
a list of choices.
......@@ -316,6 +346,53 @@ def start_wizard(session):
title="Image Description", default=session['metadata']['DESCRIPTION']
if 'DESCRIPTION' in session['metadata'] else '')
# Create VirtIO Installation Page
def display_installed_drivers():
"""Returnes the installed virtio drivers"""
versions = virtio_versions(image.os.virtio_state)
ret = "Installed Block Device Driver: %(netkvm)s\n" \
"Installed Network Device Driver: %(viostor)s\n" % versions
virtio = image.os.sysprep_params['virtio'].value
if virtio:
ret += "\nNew Block Device Driver: %(netkvm)s\n" \
"New Network Device Driver: %(viostor)s\n" % \
virtio_versions(image.os.compute_virtio_state(virtio))
return ret
def validate_virtio(_):
netkvm = len(image.os.virtio_state['netkvm']) != 0
viostor = len(image.os.virtio_state['viostor']) != 0
drv_dir = image.os.sysprep_params['virtio'].value
if netkvm is False or viostor is False:
new = image.os.compute_virtio_state(drv_dir) if drv_dir else None
new_viostor = len(new['viostor']) != 0 if new else False
new_netkvm = len(new['netkvm']) != 0 if new else False
d = session['dialog']
title = "VirtIO driver missing"
msg = "Image creation cannot proceed unless a VirtIO %s driver " \
"is installed on the media!"
if not (viostor or new_viostor):
d.msgbox(msg % "Block Device", width=PAGE_WIDTH,
height=PAGE_HEIGHT, title=title)
raise WizardReloadPage
if not(netkvm or new_netkvm):
d.msgbox(msg % "Network Device", width=PAGE_WIDTH,
height=PAGE_HEIGHT, title=title)
raise WizardReloadPage
return drv_dir
virtio = WizardInfoPage(
"virtio", "VirtIO Drivers Path",
"Press <New> to install new VirtIO drivers.",
display_installed_drivers, title="VirtIO Drivers", extra_label='New',
extra=lambda: update_sysprep_param(session, 'virtio'),
validate=validate_virtio)
# Create Image Registration Wizard Page
def registration_choices():
return [("Private", "Image is accessible only by this user"),
......@@ -326,14 +403,16 @@ def start_wizard(session):
"Please provide a registration type:", registration_choices,
title="Registration Type", default="Private")
w = Wizard(session)
wizard = Wizard(session)
w.add_page(cloud)
w.add_page(name)
w.add_page(descr)
w.add_page(registration)
wizard.add_page(cloud)
wizard.add_page(name)
wizard.add_page(descr)
if hasattr(image.os, 'install_virtio_drivers'):
wizard.add_page(virtio)
wizard.add_page(registration)
if w.run():
if wizard.run():
create_image(session)
else:
return False
......@@ -353,6 +432,9 @@ def create_image(session):
try:
out.clear()
if 'virtio' in wizard and image.os.sysprep_params['virtio'].value:
image.os.install_virtio_drivers()
# Sysprep
image.os.do_sysprep()
metadata = image.os.meta
......
......@@ -174,6 +174,7 @@ class Image(object):
# process was introduced in version 1.19.16
if self.check_guestfs_version(1, 19, 16) >= 0:
self.g.shutdown()
self.g.close()
else:
self.g.kill_subprocess()
......
......@@ -104,13 +104,16 @@ def parse_options(input_args):
"input media", default=[], action="append",
metavar="SYSPREP")
parser.add_option("--install-virtio", dest="virtio", type="string",
help="install VirtIO drivers hosted under DIR "
"(Windows only)", metavar="DIR")
parser.add_option("--print-sysprep-params", dest="print_sysprep_params",
default=False, action="store_true",
help="print the defined system preparation parameters "
"for this input media")
parser.add_option("--sysprep-param", dest="sysprep_params", default=[],
help="Add KEY=VALUE system preparation parameter",
help="add KEY=VALUE system preparation parameter",
action="append")
parser.add_option("--no-sysprep", dest="sysprep", default=True,
......@@ -125,7 +128,7 @@ def parse_options(input_args):
action="store_true")
parser.add_option("--allow-unsupported", dest="allow_unsupported",
help="Proceed with the image creation even if the media "
help="proceed with the image creation even if the media "
"is not supported", default=False, action="store_true")
parser.add_option("--tmpdir", dest="tmp", type="string", default=None,
......@@ -174,6 +177,10 @@ def parse_options(input_args):
raise FatalError("Sysprep parameter option: `%s' is not in "
"KEY=VALUE format." % p)
sysprep_params[key] = value
if options.virtio is not None:
sysprep_params['virtio'] = options.virtio
options.sysprep_params = sysprep_params
return options
......@@ -289,6 +296,10 @@ def image_creator():
if options.outfile is None and not options.upload:
return 0
if options.virtio is not None and \
hasattr(image.os, 'install_virtio_drivers'):
image.os.install_virtio_drivers()
if options.sysprep:
image.os.do_sysprep()
......
......@@ -84,7 +84,7 @@ def sysprep(message, enabled=True, **kwargs):
class SysprepParam(object):
"""This class represents a system preparation parameter"""
def __init__(self, type, default, description, check=lambda x: x):
def __init__(self, type, default, description, **kargs):
assert hasattr(self, "_check_%s" % type), "Invalid type: %s" % type
......@@ -93,7 +93,8 @@ class SysprepParam(object):
self.description = description
self.value = default
self.error = None
self.check = check
self.check = kargs['check'] if 'check' in kargs else lambda x: x
self.hidden = kargs['hidden'] if 'hidden' in kargs else False
def set_value(self, value):
"""Update the value of the parameter"""
......@@ -156,10 +157,12 @@ class SysprepParam(object):
raise ValueError("Invalid dirname")
def add_sysprep_param(name, type, default, descr, check=lambda x: x):
def add_sysprep_param(name, type, default, descr, **kargs):
"""Decorator for __init__ that adds the definition for a system preparation
parameter in an instance of an os_type class
"""
extra = kargs
def wrapper(init):
@wraps(init)
def inner(self, *args, **kwargs):
......@@ -167,8 +170,8 @@ def add_sysprep_param(name, type, default, descr, check=lambda x: x):
if not hasattr(self, 'sysprep_params'):
self.sysprep_params = {}
self.sysprep_params[name] = SysprepParam(type, default, descr,
check)
self.sysprep_params[name] = \
SysprepParam(type, default, descr, **extra)
init(self, *args, **kwargs)
return inner
return wrapper
......@@ -202,16 +205,20 @@ class OSBase(object):
if 'sysprep_params' in kargs:
for key, val in kargs['sysprep_params'].items():
if key not in self.sysprep_params:
self.out.warn("Ignoring invalid `%s' parameter." % key)
continue
param = self.sysprep_params[key]
if not param.set_value(val):
raise FatalError("Invalid value for sysprep parameter: "
"`%s'. Reason: %s" % (key, param.error))
self.meta = {}
self.mounted = False
# This will host the error if mount fails
self._mount_error = ""
self._mount_warnings = []
self._mounted = False
# Many guestfs compilations don't support scrub
self._scrub_support = True
......@@ -220,6 +227,29 @@ class OSBase(object):
except RuntimeError:
self._scrub_support = False
self._cleanup_jobs = {}
def _add_cleanup(self, namespace, job, *args):
"""Add a new job in a cleanup list"""
if namespace not in self._cleanup_jobs:
self._cleanup_jobs[namespace] = []
self._cleanup_jobs[namespace].append((job, args))
def _cleanup(self, namespace):
"""Run the cleanup tasks that are defined under a specific namespace"""
if namespace not in self._cleanup_jobs:
self.out.warn("Cleanup namespace: `%s' is not defined", namespace)
return
while len(self._cleanup_jobs[namespace]):
job, args = self._cleanup_jobs[namespace].pop()
job(*args)
del self._cleanup_jobs[namespace]
def inspect(self):
"""Inspect the media to check if it is supported"""
......@@ -227,27 +257,19 @@ class OSBase(object):
return
self.out.output('Running OS inspection:')
try:
if not self.mount(readonly=True, silent=True):
raise FatalError("Unable to mount the media read-only")
with self.mount(readonly=True, silent=True):
self._do_inspect()
finally:
self.umount(silent=True)
self.out.output()
def collect_metadata(self):
"""Collect metadata about the OS"""
try:
if not self.mount(readonly=True, silent=True):
raise FatalError("Unable to mount the media read-only")
self.out.output('Collecting image metadata ...', False)
self.out.output('Collecting image metadata ...', False)
with self.mount(readonly=True, silent=True):
self._do_collect_metadata()
self.out.success('done')
finally:
self.umount(silent=True)
self.out.success('done')
self.out.output()
def list_syspreps(self):
......@@ -327,7 +349,9 @@ class OSBase(object):
self.out.output("System preparation parameters:")
self.out.output()
if len(self.sysprep_params) == 0:
public_params = [(n, p) for n, p in self.sysprep_params.items()
if not p.hidden]
if len(public_params) == 0:
self.out.output("(none)")
return
......@@ -335,7 +359,9 @@ class OSBase(object):
wrapper.subsequent_indent = " "
wrapper.width = 72
for name, param in self.sysprep_params.items():
for name, param in public_params:
if param.hidden:
continue
self.out.output("NAME: %s" % name)
self.out.output("VALUE: %s" % param.value)
self.out.output(
......@@ -352,12 +378,7 @@ class OSBase(object):
"System preparation is disabled for unsupported media")
return
try:
if not self.mount(readonly=False):
msg = "Unable to mount the media read-write. Reason: %s" % \
self._mount_error
raise FatalError(msg)
with self.mount():
enabled = [task for task in self.list_syspreps() if task.enabled]
size = len(enabled)
......@@ -367,39 +388,74 @@ class OSBase(object):
self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
task()
setattr(task.im_func, 'executed', True)
finally:
self.umount()
self.out.output()
def mount(self, readonly=False, silent=False):
"""Mount image."""
if getattr(self, "mounted", False):
return True
mount_type = 'read-only' if readonly else 'read-write'
if not silent:
self.out.output("Mounting the media %s ..." % mount_type, False)
self._mount_error = ""
if not self._do_mount(readonly):
return False
self.mounted = True
if not silent:
self.out.success('done')
return True