Commit d077193c authored by Nikos Skalkotos's avatar Nikos Skalkotos

Merge branch 'master' into debian-precise

parents 06212455 77d2b6b1
2015-03-02, v0.9
* Add Linux syspreps for disabling the IPv6 privacy extensions and for
changing the boot timeout
* Add support for syslinux
* Add support for outputting to syslog
* Fix bugs
2015-02-05, v0.8.1
* Fix a bug in the wizard that terminated the program unexpectedly
......
......@@ -18,11 +18,19 @@ Please see the [official Synnefo site](http://www.synnefo.org) and the
[latest snf-image-creator docs](http://www.synnefo.org/docs/snf-image-creator/latest/index.html)
for more information.
Contact
-------
For questions or bug reports you can contact the Synnefo team at the following
mailing lists:
Users list: synnefo@googlegroups.com
Developers list: synnefo-devel@googlegroups.com
Copyright and license
=====================
Copyright (C) 2011-2014 GRNET S.A.
Copyright (C) 2011-2015 GRNET S.A.
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
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -36,6 +36,7 @@ from image_creator.util import FatalError
from image_creator.output.cli import SimpleOutput
from image_creator.output.dialog import GaugeOutput
from image_creator.output.composite import CompositeOutput
from image_creator.output.syslog import SyslogOutput
from image_creator.disk import Disk
from image_creator.dialog_wizard import start_wizard
from image_creator.dialog_menu import main_menu
......@@ -160,9 +161,14 @@ def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[],
return (code, output.splitlines())
def dialog_main(media, logfile, tmpdir, snapshot):
def dialog_main(media, **kwargs):
"""Main function for the dialog-based version of the program"""
tmpdir = kwargs['tmpdir'] if 'tmpdir' in kwargs else None
snapshot = kwargs['snapshot'] if 'snapshot' in kwargs else True
logfile = kwargs['logfile'] if 'logfile' in kwargs else None
syslog = kwargs['syslog'] if 'syslog' in kwargs else False
# In openSUSE dialog is buggy under xterm
if os.environ['TERM'] == 'xterm':
os.environ['TERM'] = 'linux'
......@@ -204,20 +210,26 @@ def dialog_main(media, logfile, tmpdir, snapshot):
tmplog = None if logfile else tempfile.NamedTemporaryFile(prefix='fatal-',
delete=False)
logs = []
try:
stream = logfile if logfile else tmplog
log = SimpleOutput(colored=False, stderr=stream, stdout=stream)
logs.append(SimpleOutput(colored=False, stderr=stream, stdout=stream))
if syslog:
logs.append(SyslogOutput())
while 1:
try:
out = CompositeOutput([log])
out = CompositeOutput(logs)
out.info("Starting %s v%s ..." % (PROGNAME, version))
ret = create_image(d, media, out, tmpdir, snapshot)
break
except Reset:
log.info("Resetting everything ...")
for log in logs:
log.info("Resetting everything ...")
except FatalError as error:
log.error(str(error))
for log in logs:
log.error(str(error))
msg = 'A fatal error occured. See %s for a full log.' % log.stderr.name
d.infobox(msg, width=WIDTH, title="Fatal Error")
return 1
......@@ -246,6 +258,8 @@ def main():
help="don't snapshot the input media. (THIS IS "
"DANGEROUS AS IT WILL ALTER THE ORIGINAL MEDIA!!!)",
action="store_false")
parser.add_option("--syslog", dest="syslog", default=False,
help="log to syslog", action="store_true")
parser.add_option("--tmpdir", type="string", dest="tmp", default=None,
help="create large temporary image files under DIR",
metavar="DIR")
......@@ -271,7 +285,8 @@ def main():
# Save the terminal attributes
attr = termios.tcgetattr(sys.stdin.fileno())
try:
ret = dialog_main(media, logfile, opts.tmp, opts.snapshot)
ret = dialog_main(media, logfile=logfile, tmpdir=opts.tmp,
snapshot=opts.snapshot, syslog=opts.syslog)
finally:
# Restore the terminal attributes. If an error occurs make sure
# that the terminal turns back to normal.
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -38,20 +38,19 @@ from image_creator.dialog_util import SMALL_WIDTH, WIDTH, \
copy_file
CONFIGURATION_TASKS = [
("Partition table manipulation", ["FixPartitionTable"],
["linux", "windows"]),
("Partition table manipulation", ["FixPartitionTable"], lambda x: True),
("File system resize",
["FilesystemResizeUnmounted", "FilesystemResizeMounted"],
["linux", "windows"]),
("Swap partition configuration", ["AddSwap"], ["linux"]),
("SSH keys removal", ["DeleteSSHKeys"], ["linux"]),
["FilesystemResizeUnmounted", "FilesystemResizeMounted"], lambda x: True),
("Swap partition configuration", ["AddSwap"], lambda x: x == 'linux'),
("SSH keys removal", ["DeleteSSHKeys"], lambda x: x != 'windows'),
("Temporal RDP disabling", ["DisableRemoteDesktopConnections"],
["windows"]),
("SELinux relabeling at next boot", ["SELinuxAutorelabel"], ["linux"]),
("Hostname/Computer Name assignment", ["AssignHostname"],
["windows", "linux"]),
("Password change", ["ChangePassword"], ["windows", "linux"]),
("File injection", ["EnforcePersonality"], ["windows", "linux"])
lambda x: x == "windows"),
("SELinux relabeling at next boot", ["SELinuxAutorelabel"],
lambda x: x == "linux"),
("Hostname/Computer Name assignment", ["AssignHostname"], lambda x: True),
("Password change", ["ChangePassword"], lambda x: True),
("Network configuration", ["ConfigureNetwork"], lambda x: x != 'windows'),
("File injection", ["EnforcePersonality"], lambda x: True)
]
SYSPREP_PARAM_MAXLEN = 20
......@@ -641,14 +640,18 @@ def exclude_tasks(session):
else:
return False
for (msg, task, osfamily) in CONFIGURATION_TASKS:
if image.meta['OSFAMILY'] in osfamily:
for (msg, task, os_check) in CONFIGURATION_TASKS:
if os_check(image.meta['OSFAMILY']):
checked = 1 if index in session['excluded_tasks'] else 0
choices.append((str(displayed_index), msg, checked))
mapping[displayed_index] = index
displayed_index += 1
index += 1
if len(choices) == 0:
d.msgbox("No configuration tasks available", width=WIDTH)
return True
while 1:
text = "Please choose which configuration tasks you would like to " \
"prevent from running during image deployment. " \
......
......@@ -386,10 +386,11 @@ def update_sysprep_param(session, name, title=None):
default_item = 1
while 1:
value = []
for i in param.value:
value.append(i)
if param.is_list:
value = []
for i in param.value:
value.append(i)
choices = [(str(i+1), str(value[i])) for i in xrange(len(value))]
if len(choices) == 0:
action = 'add'
......@@ -433,6 +434,8 @@ def update_sysprep_param(session, name, title=None):
del value[choice-1]
else:
value[choice-1] = new_value
else:
value = new_value
if param.set_value(value) is False:
d.msgbox("Error: %s" % param.error, width=WIDTH)
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -21,8 +21,12 @@ from image_creator.util import FatalError, QemuNBD, get_command
from image_creator.gpt import GPTPartitionTable
from image_creator.os_type import os_cls
import re
import os
# Make sure libguestfs runs qemu directly to launch an appliance.
os.environ['LIBGUESTFS_BACKEND'] = 'direct'
import guestfs
import re
import hashlib
from sendfile import sendfile
import threading
......@@ -357,10 +361,17 @@ class Image(object):
part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
if self.check_guestfs_version(1, 15, 17) >= 0:
self.g.e2fsck(part_dev, forceall=1)
else:
self.g.e2fsck_f(part_dev)
try:
if self.check_guestfs_version(1, 15, 17) >= 0:
self.g.e2fsck(part_dev, forceall=1)
else:
self.g.e2fsck_f(part_dev)
except RuntimeError as e:
# There is a bug in some versions of libguestfs and a RuntimeError
# is thrown although the command has successfully corrected the
# found file system errors.
if e.message.find('***** FILE SYSTEM WAS MODIFIED *****') == -1:
raise
self.g.resize2fs_M(part_dev)
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -25,6 +25,8 @@ from image_creator.disk import Disk
from image_creator.util import FatalError
from image_creator.output.cli import SilentOutput, SimpleOutput, \
OutputWthProgress
from image_creator.output.composite import CompositeOutput
from image_creator.output.syslog import SyslogOutput
from image_creator.kamaki_wrapper import Kamaki, ClientError
import sys
import os
......@@ -135,6 +137,9 @@ def parse_options(input_args):
parser.add_option("-s", "--silent", dest="silent", default=False,
help="output only errors", action="store_true")
parser.add_option('--syslog', dest="syslog", default=False,
help="log to syslog", action="store_true")
parser.add_option("--sysprep-param", dest="sysprep_params", default=[],
help="add KEY=VALUE system preparation parameter",
action="append")
......@@ -158,31 +163,30 @@ def parse_options(input_args):
options.source = args[0]
if not os.path.exists(options.source):
raise FatalError("Input media `%s' is not accessible" % options.source)
parser.error("Input media `%s' is not accessible" % options.source)
if options.register and not options.upload:
raise FatalError("You also need to set -u when -r option is set")
parser.error("You also need to set -u when -r option is set")
if options.upload and (options.token is None or options.url is None) and \
options.cloud is None:
err = "You need to either specify an authentication URL and token " \
"pair or an available cloud name."
raise FatalError("Image uploading cannot be performed. %s" % err)
parser.error("Image uploading cannot be performed. You need to either "
"specify an authentication URL and token pair or an "
"available cloud name.")
if options.tmp is not None and not os.path.isdir(options.tmp):
raise FatalError("The directory `%s' specified with --tmpdir is not "
"valid" % options.tmp)
parser.error("The directory `%s' specified with --tmpdir is not valid"
% options.tmp)
meta = {}
for m in options.metadata:
try:
key, value = m.split('=', 1)
except ValueError:
raise FatalError("Metadata option: `%s' is not in KEY=VALUE "
"format." % m)
meta[key] = value
parser.error("Metadata option: `%s' is not in KEY=VALUE format." %
m)
meta[key.upper()] = value
options.metadata = meta
sysprep_params = {}
......@@ -190,50 +194,38 @@ def parse_options(input_args):
try:
key, value = p.split('=', 1)
except ValueError:
raise FatalError("Sysprep parameter option: `%s' is not in "
"KEY=VALUE format." % p)
parser.error("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
def image_creator():
"""snf-mkimage main function"""
options = parse_options(sys.argv[1:])
if options.outfile is None and not options.upload and not \
options.print_syspreps and not options.print_sysprep_params \
and not options.print_metadata:
raise FatalError("At least one of `-o', `-u', `--print-syspreps', "
"`--print-sysprep-params' or `--print-metadata' must "
"be set")
parser.error("At least one of `-o', `-u', `--print-syspreps', "
"`--print-sysprep-params' or `--print-metadata' must be "
"set")
if options.silent:
out = SilentOutput(colored=sys.stderr.isatty())
else:
out = OutputWthProgress() if sys.stderr.isatty() else \
SimpleOutput(colored=False)
if not options.force and options.outfile is not None:
for extension in ('', '.meta', '.md5sum'):
filename = "%s%s" % (options.outfile, extension)
if os.path.exists(filename):
parser.error("Output file `%s' exists (use --force to "
"overwrite it)." % filename)
return options
title = 'snf-image-creator %s' % version
out.info(title)
out.info('=' * len(title))
def image_creator(options, out):
"""snf-mkimage main function"""
if os.geteuid() != 0:
raise FatalError("You must run %s as root"
% os.path.basename(sys.argv[0]))
if not options.force and options.outfile is not None:
for extension in ('', '.meta', '.md5sum'):
filename = "%s%s" % (options.outfile, extension)
if os.path.exists(filename):
raise FatalError("Output file `%s' exists "
"(use --force to overwrite it)." % filename)
# Check if the authentication info is valid. The earlier the better
if options.token is not None and options.url is not None:
try:
......@@ -309,11 +301,21 @@ def image_creator():
if not os.access(script, os.X_OK):
raise FatalError("File: `%s' is not executable." % script)
for sysprep in options.disabled_syspreps:
image.os.disable_sysprep(image.os.get_sysprep_by_name(sysprep))
for name in options.disabled_syspreps:
sysprep = image.os.get_sysprep_by_name(name)
if sysprep is not None:
image.os.disable_sysprep(sysprep)
else:
out.warn("Sysprep: `%s' does not exist. Can't disable it." %
name)
for sysprep in options.enabled_syspreps:
image.os.enable_sysprep(image.os.get_sysprep_by_name(sysprep))
for name in options.enabled_syspreps:
sysprep = image.os.get_sysprep_by_name(name)
if sysprep is not None:
image.os.enable_sysprep(sysprep)
else:
out.warn("Sysprep: `%s' does not exist. Can't enable it." %
name)
if options.print_syspreps:
image.os.print_syspreps()
......@@ -447,10 +449,25 @@ def image_creator():
def main():
"""Main entry point"""
options = parse_options(sys.argv[1:])
if options.silent:
out = SilentOutput(colored=sys.stderr.isatty())
else:
out = OutputWthProgress() if sys.stderr.isatty() else \
SimpleOutput(colored=False)
if options.syslog:
out = CompositeOutput([out, SyslogOutput()])
title = 'snf-image-creator %s' % version
out.info(title)
out.info('=' * len(title))
try:
sys.exit(image_creator())
sys.exit(image_creator(options, out))
except FatalError as e:
SimpleOutput(colored=sys.stderr.isatty()).error(e)
out.error(e)
sys.exit(1)
if __name__ == '__main__':
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -48,6 +48,8 @@ def os_cls(distro, osfamily):
distro = canonicalize(distro)
osfamily = canonicalize(osfamily)
if distro == 'unknown':
distro = osfamily
try:
module = __import__("image_creator.os_type.%s" % distro,
......@@ -88,7 +90,7 @@ def sysprep(message, enabled=True, **kwargs):
@wraps(method)
def inner(self, print_message=True):
if print_message:
self.out.info(message)
self.out.info(message % self.sysprep_params)
return method(self)
return inner
......@@ -113,6 +115,10 @@ class SysprepParam(object):
assert hasattr(self, "_check_%s" % self.type), \
"Invalid type: %s" % self.type
def __str__(self):
"""Return the value as a string"""
return str(self.value)
def set_value(self, value):
"""Update the value of the parameter"""
......@@ -341,8 +347,6 @@ class OSBase(object):
def get_sysprep_by_name(self, name):
"""Returns the sysprep object with the given name"""
error_msg = "Syprep operation %s does not exist for %s" % \
(name, self.__class__.__name__)
method_name = '_' + name.replace('-', '_')
......@@ -352,7 +356,7 @@ class OSBase(object):
if hasattr(method, '_sysprep'):
return method
raise FatalError(error_msg)
return None
def enable_sysprep(self, obj):
"""Enable a system preparation operation"""
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -29,6 +29,11 @@ class Bsd(Unix):
def _cleanup_password(self):
"""Remove all passwords and lock all user accounts"""
if not self.image.g.is_file('/etc/master.passwd'):
self.out.warn(
"File: `/etc/master.passwd' is missing. Nothing to do!")
return
master_passwd = []
for line in self.image.g.cat('/etc/master.passwd').splitlines():
......@@ -101,6 +106,12 @@ class Bsd(Unix):
def _get_passworded_users(self):
"""Returns a list of non-locked user accounts"""
if not self.image.g.is_file('/etc/master.passwd'):
self.out.warn("Unable to collect user info. "
"File: `/etc/master.passwd' is missing!")
return []
users = []
regexp = re.compile(
'^([^:]+):((?:![^:]+)|(?:[^!*][^:]+)|):(?:[^:]*:){7}(?:[^:]*)'
......
This diff is collapsed.
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 GRNET S.A.
# Copyright (C) 2011-2015 GRNET S.A.
#
# 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
......@@ -235,8 +235,8 @@ class Windows(OSBase):
@add_sysprep_param(
'connection_retries', "posint", 5, DESCR['connection_retries'])
@add_sysprep_param(
'shutdown_timeout', "posint", 120, DESCR['shutdown_timeout'])
@add_sysprep_param('boot_timeout', "posint", 300, DESCR['boot_timeout'])
'shutdown_timeout', "posint", 300, DESCR['shutdown_timeout'])
@add_sysprep_param('boot_timeout', "posint", 600, DESCR['boot_timeout'])
@add_sysprep_param('virtio', 'dir', "", DESCR['virtio'],
check=virtio_dir_check, hidden=True)
@add_sysprep_param(
......@@ -863,7 +863,7 @@ class Windows(OSBase):
return collection
def install_virtio_drivers(self, upgrade=True):
"""Install new VirtIO drivers in the input media. If upgrade is True,
"""Install new VirtIO drivers on the input media. If upgrade is True,
then the old drivers found in the media will be removed.
"""
......@@ -878,9 +878,37 @@ class Windows(OSBase):
self.out.warn('No suitable driver found to install!')
return
remove = []
certs = []
install = []
add = []
# Check which drivers we need to install, which to add to the database
# and which to remove.
for dtype in valid_drvs:
versions = [v['DriverVer'] for k, v in valid_drvs[dtype].items()]
certs.extend([v['CatalogFile'] for k, v in
valid_drvs[dtype].items() if 'CatalogFile' in v])
installed = [(k, v['DriverVer']) for k, v in
self.virtio_state[dtype].items()]
found = [d[0] for d in installed if d[1] in versions]
not_found = [d[0] for d in installed if d[1] not in versions]
for drvr in found:
details = self.virtio_state[dtype][drvr]
self.out.warn('%s driver with version %s is already installed!'
% (dtype, details['DriverVer']))
if upgrade:
remove.extend(not_found)
if dtype == 'viostor':
install.extend([d for d in valid_drvs[dtype]])
else:
add.extend([d for d in valid_drvs[dtype]])
try:
self._upload_virtio_drivers(dirname, valid_drvs, upgrade)
self._boot_virtio_vm()
self._update_driver_database('virtio', upload=dirname, certs=certs,
add=add, install=install,
remove=remove)
finally:
with self.mount(readonly=False, silent=True, fatal=False):
if not self.ismounted:
......@@ -892,39 +920,57 @@ class Windows(OSBase):
self.out.success("VirtIO drivers were successfully installed")
self.out.info()
def _upload_virtio_drivers(self, dirname, drvs, delete_old=True):
"""Upload the VirtIO drivers and installation scripts to the media.
def _update_driver_database(self, namespace, **kwargs):
"""Upload a directory that contains the VirtIO drivers and add scripts
for installing and removing specific drivers.
Keyword arguments:
namespace -- namespace for the cleanup entries
upload -- Host directory that contains drivers to upload
add -- List of drivers to add to the driver database
install -- List of drivers to install to the system
remove -- List of drivers to remove from the system
"""
upload = kwargs['upload'] if 'upload' in kwargs else None
add = kwargs['add'] if 'add' in kwargs else []
install = kwargs['install'] if 'install' in kwargs else []
certs = kwargs['certs'] if 'certs' in kwargs else []
remove = kwargs['remove'] if 'remove' in kwargs else []
assert len(add) == 0 or upload is not None
assert len(install) == 0 or upload is not None
with self.mount(readonly=False, silent=True):
# Reset Password
self._add_cleanup('virtio', self.registry.reset_passwd,
# Reset admin password
self._add_cleanup(namespace, self.registry.reset_passwd,
self.vm.admin.rid,
self.registry.reset_passwd(self.vm.admin.rid))
# Enable admin account (if needed)
self._add_cleanup('virtio', self.registry.reset_account,
self._add_cleanup(namespace, self.registry.reset_account,
self.vm.admin.rid,
self.registry.reset_account(self.vm.admin.rid))
old = self.registry.update_uac(0)
if old != 0:
self._add_cleanup('virtio', self.registry.update_uac, old)