Commit 33a97c41 authored by Nikos Skalkotos's avatar Nikos Skalkotos

Merge branch 'release-0.9' into debian-release-0.9

parents ebca35f0 c0a4a74e
2015-02-24, v0.9rc1
* 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"""
......
......@@ -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.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
......@@ -24,15 +24,15 @@ of the various parts of the image-creator package.
class Output(object):
"""A class for printing program output"""
def error(self, msg, new_line=True):
def error(self, msg):
"""Print an error"""
pass
def warn(self, msg, new_line=True):
def warn(self, msg):
"""Print a warning"""
pass
def success(self, msg, new_line=True):
def success(self, msg):
"""Print msg after an action is completed"""
pass
......@@ -40,7 +40,7 @@ class Output(object):
"""Print normal program output"""
pass
def result(self, msg='', new_line=True):
def result(self, msg=''):
"""Print a result"""
pass
......
# -*- 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
......@@ -39,14 +39,14 @@ class SilentOutput(Output):
self.stdout = kwargs['stdout'] if 'stdout' in kwargs else sys.stdout
self.stderr = kwargs['stderr'] if 'stderr' in kwargs else sys.stderr
def result(self, msg, new_line=True):
def result(self, msg):
"""Print a result"""
write(msg, new_line, lambda x: x, self.stdout)
write(msg, True, lambda x: x, self.stdout)
def error(self, msg, new_line=True):
def error(self, msg):
"""Print an error"""
color = red if self.colored else lambda x: x
write("Error: %s" % msg, new_line, color, self.stderr)
write("Error: %s" % msg, True, color, self.stderr)
class SimpleOutput(SilentOutput):
......@@ -54,15 +54,15 @@ class SimpleOutput(SilentOutput):
output messages. The user gets informed when the action begins and when it
ends, but no progress is shown in between."""
def warn(self, msg, new_line=True):
def warn(self, msg):
"""Print a warning"""
color = yellow if self.colored else lambda x: x
write("Warning: %s" % msg, new_line, color, self.stderr)
write("Warning: %s" % msg, True, color, self.stderr)
def success(self, msg, new_line=True):
def success(self, msg):
"""Print msg after an action is completed"""
color = green if self.colored else lambda x: x
write(msg, new_line, color, self.stderr)
write(msg, True, color, self.stderr)
def info(self, msg='', new_line=True):
"""Print msg as normal program output"""
......
# -*- 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,30 +36,30 @@ class CompositeOutput(Output, list):
if outputs is not None:
self.extend(outputs)
def error(self, msg, new_line=True):
def error(self, msg):
"""Call the error method of each of the output instances"""
for out in self:
out.error(msg, new_line)
out.error(msg)
def warn(self, msg, new_line=True):
def warn(self, msg):
"""Call the warn method of each of the output instances"""
for out in self:
out.warn(msg, new_line)
out.warn(msg)
def success(self, msg, new_line=True):
def success(self, msg):
"""Call the success method of each of the output instances"""
for out in self:
out.success(msg, new_line)
out.success(msg)
def info(self, msg='', new_line=True):
"""Call the output method of each of the output instances"""
for out in self:
out.info(msg, new_line)
def result(self, msg='', new_line=True):