# Ubuntu Testing Automation Harness
# Copyright 2012 Canonical Ltd.
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
"""
Provide common functions for provisioning and interacting with machines.
Functions here should apply to multiple machine types (VM, bare metal, etc.)
"""
import logging
import logging.handlers
import os
import pipes
import re
import shutil
import sys
import urllib
import uuid
import netifaces
import utah.timeout
from utah import template
from utah.cleanup import cleanup
from utah.config import config
from utah.process import ProcessRunner
from utah.preseed import Preseed
from utah.provisioning.debs import get_client_debs
from utah.provisioning.rsyslog import RSyslog
from utah.provisioning.exceptions import UTAHProvisioningException
from utah.retry import retry
[docs]class Machine(object):
# TODO: check if we need a general restart method
"""Provide a generic class to provision an arbitrary machine.
Raise exceptions for most methods, since subclasses should provide them.
run, uploadfiles, and downloadfiles are the most important methods
for interacting with a machine.
Some method of satisfying activecheck is generally required to run
commands on a machine (i.e., implementing _start or reimplementing
activecheck.)
Some method of satisfying provisioncheck is generally required to install
on a machine (i.e., implementing some combination of _create, _load, and
_provision, or reimplementing provisioncheck.)
Installation may be separated from the Machine classes in the future.
"""
def __init__(self, boot=config.boot, clean=True, debug=False,
image=None, initrd=config.initrd, inventory=None,
kernel=config.kernel, machineid=config.machineid,
machineuuid=config.machineuuid, name=config.name, new=False,
preseed=config.preseed, rewrite=config.rewrite,
template=config.template, xml=config.xml):
"""Initialize the object representing the machine.
One of these groups of arguments should be included:
image, kernel, initrd, preseed: Install the machine from a
specified image object, with an optional kernel, initrd, and
preseed. libvirt xml file can be passed in xml as well.
template: Clone the machine from a template or existing machine.
name: Request a specific machine. Combine with other groups to
reinstall a specific machine.
The subclass is responsible for provisioning with these arguments.
Other arguments:
clean: Enable cleanup functions.
debug: Enable debug logging.
inventory: Inventory object managing the machine; used for cleanup
purposes.
machineid: Should be passed by the request method of an Inventory
class. Often used to name virtual machines.
machineuuid: Unique identifier. Will be randomly generated if not
supplied.
new: Request a new machine (or a reinstall if a specific machine
was requested.)
rewrite: How much to alter supplied preseed and xml files.
all: everything we need for an automated install
minimal: insert latecommand into preseed
insert preseed into casper (desktop only)
edit VM XML to handle reboots properly
casperonly: insert preseed into casper (desktop only)
none: do not alter preseed or VM XML
| preseed | casper | VM xml | other |
----------+---------+--------+--------+-------+
all | X | X | X | X |
minimal | X | X | X | |
casperonly| | X | | |
none | | | | |
"""
# TODO: Make this work right with super at some point.
# TODO: Consider a global temp file creator, maybe as part of install.
self.boot = boot
self.clean = clean
self.debug = debug
self.image = image
self.inventory = inventory
self.machineid = machineid
self.new = new
self.rewrite = rewrite
self.template = template
self.uuid = uuid
# TODO: Move namesetup into vm
self._namesetup(name)
if machineuuid is None:
self.uuid = str(uuid.uuid4())
else:
self.uuid = machineuuid
self.provisioned = False
self.active = False
self._loggersetup()
fileargs = ['initrd', 'kernel', 'preseed', 'xml']
# TODO: make this a function that lives somewhere else:
# TODO: maybe move this preparation out of this class
for item in fileargs:
# Ensure every file/url type argument is available locally
arg = locals()[item]
if arg is None:
setattr(self, item, None)
else:
if arg.startswith('~'):
path = os.path.expanduser(arg)
self.logger.debug('Rewriting ~-based path: %s to: %s',
arg, path)
else:
path = arg
self.percent = 0
self.logger.info('Preparing %s: %s', item, path)
# TODO: implement download utility function and use it here
filename = urllib.urlretrieve(path,
reporthook=self.dldisplay)[0]
setattr(self, item, filename)
self.logger.debug('%s is locally available as %s',
path, getattr(self, item))
self.finalpreseed = self.preseed
self.logger.debug('Machine init finished')
@property
[docs] def rsyslog(self):
"""Use the default rsyslog method if no override is in place.
:returns: rsyslog method for this machine
:rtype: object
"""
if not getattr(self, '_rsyslog', None):
self._rsyslog = RSyslog(self.name, config.logpath)
return self._rsyslog
def _namesetup(self, name=None):
"""Name the machine, automatically or using a specified name.
:param name: Name to use for this machine
:type name: str
"""
if name is None:
self.name = self._makename()
else:
self.name = name
def _makename(self, machineid=None, prefix=None):
"""Return a name for the machine based on how it will be installed.
Generally used for VMs to comply with old vm-tools naming conventions.
Probably could be reduced or removed.
:param machineid: Unique numerical machine identifier
:type machineid: int
:param prefix: Prefix for machine name (defaults to utah)
:type prefix: str
:returns: Generated machine name, like utah-1-precise-i386
:rtype: str
"""
if machineid is None:
machineid = str(self.machineid)
if prefix is None:
prefix = str(self.prefix)
if machineid is not None:
prefix += ('-{}'.format(str(machineid)))
self.prefix = prefix
if self.image.installtype == 'desktop':
name = '-'.join([prefix, self.image.series, self.image.arch])
else:
name = '-'.join([prefix, self.image.series,
self.image.installtype, self.image.arch])
return name
def _loggersetup(self):
"""Initialize the logging for the machine.
Subclasses can override or supplement to customize logging.
"""
self.logger = logging.getLogger(self.name)
def _loggerunsetup(self):
"""Remove logging.
Principally used when a machine changes name after init.
"""
del self.logger
[docs] def provisioncheck(self, provision_data=None):
"""Ensure the machine is provisioned and installed.
Check if the machine is provisioned, provision it if necessary, run an
install if needed, and raise an exception if it cannot be provisioned,
or if it exceeds the timeout value set in config for installation.
:raises UTAHProvisioningException:
When the machine fails to install within the timeout value.
"""
self.logger.debug('Checking if machine is provisioned')
if not self.provisioned:
utah.timeout.timeout(
config.installtimeout, self._provision, provision_data)
[docs] def activecheck(self):
"""Ensure the machine is active and capable of accepting commands.
Check if the machine is running and able to accept commands, start it
if necessary, and raise an exception if it cannot be started.
"""
self.logger.debug('Checking if machine is active')
self.provisioncheck()
if not self.active:
self._start()
[docs] def pingcheck(self):
"""Check network connectivity using ping.
:raises UTAHProvisioningException:
When ping fails.
.. seealso:: :func:`utah.retry.retry`, :meth:`pingpoll`
"""
self.logger.info('Checking network connectivity (ping)')
returncode = \
ProcessRunner(['ping', '-c1', '-w5', self.name]).returncode
if returncode != 0:
err = 'Ping returned {0}'.format(returncode)
raise UTAHProvisioningException(err, retry=True)
[docs] def pingpoll(self,
timeout=None,
checktimeout=config.checktimeout,
logmethod=None):
"""Run pingcheck over and over until timeout expires.
:param timeout: How long to try pinging the machine before giving up
:type timeout: int or None
:param checktimeout: How long to wait between pings
:type checktimeout: int
:param logmethod: Function to use for logging
:type logmethod: function
.. seealso::
:func:`utah.timeout.timeout`
:func:`utah.retry.retry`
:meth:`pingcheck`
"""
if timeout is None:
timeout = config.boottimeout
if logmethod is None:
logmethod = self.logger.debug
utah.timeout.timeout(timeout, retry, self.pingcheck,
logmethod=logmethod, retry_timeout=checktimeout)
[docs] def installclient(self):
"""Install the required packages on the machine.
Install the python-jsonschema, utah-common, and utah-client packages.
:raises UTAHProvisioningException: When packages won't install
"""
self.logger.info('Installing client deb on machine')
tmppath = os.path.normpath('/tmp')
for deb in get_client_debs():
try:
self.uploadfiles([deb], tmppath)
except UTAHProvisioningException as err:
try:
for myfile in err.files:
if deb in myfile:
raise UTAHProvisioningException(
'File not found: {filename}; '
'UTAH was probably updated during the run. '
'Please try again.'
.format(filename=deb),
retry=True)
raise err
except AttributeError:
raise err
deb = os.path.join(tmppath, os.path.basename(deb))
cmd = template.as_buff('install-deb-command.jinja2', deb=deb)
def install_client():
returncode, _stdout, stderr = self.run(cmd, root=True)
if (returncode != 0 or
re.search(r'script returned error exit status \d+',
stderr)):
raise UTAHProvisioningException('Failed to install client',
retry=True)
utah.timeout.timeout(config.client_install_timeout,
retry, install_client)
def _provision(self, provision_data):
"""Ensure the machine is available and installed.
Ready the machine for use, and set provisioned to True.
If new=True or the machine is not installed, this should install the
machine, possibly by calling _create().
If an existing machine is requested, this should make the machine
available, possibly by calling _load().
Should generally not be called directly outside of the class;
provisioncheck() or activecheck() should be used.
"""
if not self.new:
try:
self.logger.debug('Trying to load existing machine')
self._load()
self.provisioned = True
return
except Exception as err:
self.logger.debug('Failed to load machine: %s', str(err))
self._create(provision_data)
self.provisioned = True
def _create(self, provision_data):
# TODO: discuss separation of this vs. start when separating Install
"""Install the machine.
Install the operating system on the machine.
Takes no arguments, all needed variables should be set during __init__
Should generally not be called directly outside of the class;
provisioncheck() or activecheck() should be used.
:raises UTAHProvisioingException:
Currently, subclasses raise appropriate subclass of this exception
"""
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def destroy(self):
"""Free up the machine and associated resources.
Release resources consumed by the machine, set provisioned to False,
and return True on success.
For a VM, this should destroy the VM files on the disk, and remove it
from libvirt if it is registered there.
For a physical machine, this should free it up to be used by another
process in the future, and destroy sensitive data if any exists.
Destroying the install on a physical machine is optional, but the
machine must be left in a state where a new install can start.
"""
self.provisioned = False
del self
def _load(self):
"""Prepare an existing machine for use.
Should generally not be called directly outside of the class;
provisioncheck() or activecheck() should be used.
"""
# TODO: decide if this should set self.provisioned
self._unimplemented(sys._getframe().f_code.co_name)
def _start(self):
"""Start the machine, and set active to True.
Should generally not be called directly outside of the class;
activecheck() should be used.
"""
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def stop(self, _force=False):
"""Stop the machine, and set active to False.
:param force: If False, attempt graceful shutdown
:type false: bool
"""
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def uploadfiles(self, _files, _target=os.path.normpath('/tmp/')):
"""Upload a list of local files to a target on the machine.
:param files: File(s) to upload
:type files: list or string
:param target: Remote path to upload files
:type target: string
"""
# TODO: Decide whether recursion is optional, mandatory, default
# TODO: figure out if we should return some success/failure metric
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def downloadfiles(self, _files, _target=os.path.normpath('/tmp/')):
"""Download a list of files from the machine to a local target.
Support for files as a string specifying a single file is recommended
but not required.
target should be a valid target path for cp, i.e. a filename for a
single file, a directory name for multiple files.
Recursive directory download is not currently supported by all
implementations.
"""
# TODO: Decide whether recursion is optional, mandatory, default
# TODO: figure out if we should return some success/failure metric
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def run(self, _command, _quiet=None, _root=False, _timeout=None):
"""Run a command on the machine.
:param quiet: If True, suppress output from helper programs like ssh
:type quiet: bool
:param root: If True, execute command with elevated privileges
:type root: bool
:param timeout: Number of seconds to wait before timing out
:type timeout: int or None
:returns: A tuple of the form (returncode, stdout, stder)
:rtype: tuple
"""
self._unimplemented(sys._getframe().f_code.co_name)
[docs] def dldisplay(self, blocks, size, total):
"""Log download information (i.e., as a urllib callback).
:param blocks: Number of blocks downloaded
:type blocks: int
:param size: Size of blocks downloaded
:type size: int
:param total: Total size of download
:type size: int
"""
# TODO: get this and the one in iso.py into the same place
read = blocks * size
percent = 100 * read / total
if percent >= self.percent:
self.logger.info('File %s%% downloaded', percent)
self.percent += config.dlpercentincrement
self.logger.debug('%s read, %s%% of %s total', read, percent, total)
def _depcheck(self):
"""Check for dependencies that are in Recommends or Suggests.
No special dependencies exist for the main class, but we implement it
here to allow super() to work.
"""
pass
def _unimplemented(self, method):
"""Log download information (i.e., as a urllib callback).
:param blocks: Number of blocks downloaded
:type blocks: int
:param size: Size of blocks downloaded
:type size: int
:param total: Total size of download
:type size: int
"""
raise UTAHProvisioningException(
'{cls} attempted to call the {method} method '
'of the base Machine class, which is not implemented'
.format(cls=self.__class__.__name__,
method=method))
[docs] def cleanfile(self, path):
"""Register a path to be cleaned later.
:param path: A link, file, or directory to be cleaned later.
:type path: str
.. seealso:: :meth:`utah.cleanup._Cleanup.add_path`
"""
# TODO: consider doing this directly instead of through Machine
if self.clean:
cleanup.add_path(path)
[docs] def cleanfunction(self, function, *args, **kw):
"""Register a function to be run on cleanup.
:param function: A callable that will do some cleanup.
:type function: callable
:param args: Positional arguments to the function.
:type args: Tuple
:param kw: Keyword arguments to the function.
:type args: dict
.. seealso:: :meth:`utah.cleanup._Cleanup.add_function`
"""
# TODO: consider doing this directly instead of through Machine
if self.clean:
cleanup.add_function(60, function, *args, **kw)
[docs] def cleancommand(self, cmd):
"""Register a command to be run on cleanup.
:param cmd: A command as would be passed `subprocess.Popen`.
:type cmd: iterable
.. seealso:: :meth:`utah.cleanup._Cleanup.add_command`
"""
# TODO: consider doing this directly instead of through Machine
if self.clean:
cleanup.add_command(cmd)
[docs]class CustomInstallMixin(object):
"""Provide routines for automating an install from an image."""
def _preparekernel(self, kernel=None, tmpdir=None):
"""Copy a kernel to our temp directory.
If a kernel was manually specified, use that.
If we haven't been given a kernel file, unpack one from the image.
:param kernel: Local path to the kernel to use
:type filename: str
:param tmpdir: Path to temp directory
:type kernel: str
:returns: Path to kernel file in temp directory
:rtype: str
:raises UTAHProvisioningException:
If there's a problem copying the kernel file.
.. seealso:: :func:`_prepareinitrd`
"""
self.logger.info('Preparing kernel')
if kernel is None:
kernel = self.kernel
if tmpdir is None:
tmpdir = self.tmpdir
mykernel = os.path.join(tmpdir, 'kernel')
if kernel is None:
self.logger.info('Unpacking kernel from image')
kernelpath = self.image.kernelpath()
self.image.extract(kernelpath, outfile=mykernel)
else:
self.logger.info('Copying local kernel: %s', kernel)
try:
shutil.copyfile(kernel, mykernel)
except IOError as err:
raise UTAHProvisioningException(
'Failed to copy local kernel: {}'.format(err))
return mykernel
def _prepareinitrd(self, initrd=None, tmpdir=None):
"""Copy an initrd to our temp directory.
If an initrd was manually specified, use that.
If we haven't been given an initrd file, unpack one from the image.
:param initrd: Local path to the kernel to use
:type initrd: str
:param tmpdir: Path to temp directory
:type tmpdir: str
:returns: Path to initrd file in temp directory
:rtype: str
:raises UTAHProvisioningException:
If there's a problem copying the initrd file.
.. seealso:: :func:`_preparekernel`
"""
self.logger.info('Preparing initrd')
if initrd is None:
initrd = self.initrd
if tmpdir is None:
tmpdir = self.tmpdir
if initrd is None:
self.logger.info('Unpacking initrd from image')
initrdpath = './install/initrd.gz'
if self.image.installtype == 'mini':
self.logger.debug('Mini image detected')
initrdpath = 'initrd.gz'
elif self.image.installtype == 'desktop':
self.logger.debug('Desktop image detected')
# TODO: scan for this like desktop
initrdpath = './casper/initrd.lz'
myinitrd = os.path.join(tmpdir, os.path.basename(initrdpath))
self.image.extract(initrdpath, outfile=myinitrd)
else:
self.logger.info('Using local initrd: %s', initrd)
myinitrd = os.path.join(tmpdir, os.path.basename(initrd))
try:
shutil.copyfile(initrd, myinitrd)
except IOError as err:
raise UTAHProvisioningException(
'Failed to copy local initrd: {}'.format(err))
return myinitrd
def _unpackinitrd(self, initrd=None, tmpdir=None):
"""Unpack the initrd file into a directory so we can modify it."""
self.logger.info('Unpacking initrd')
if initrd is None:
initrd = self.initrd
if tmpdir is None:
tmpdir = self.tmpdir
try:
if not os.path.isdir(os.path.join(tmpdir, 'initrd.d')):
os.makedirs(os.path.join(tmpdir, 'initrd.d'))
os.chdir(os.path.join(tmpdir, 'initrd.d'))
except OSError as err:
raise UTAHProvisioningException(
'Error using temp directory {}: {}'.format(tmpdir, err))
pipe = pipes.Template()
if os.path.splitext(initrd)[1] == '.gz':
self.logger.debug('Using gzip based on file extension')
pipe.prepend('zcat $IN', 'f-')
elif os.path.splitext(initrd)[1] == '.lz':
self.logger.debug('Using lzma based on file extension')
pipe.prepend('lzcat -S .lz $IN', 'f-')
else:
raise UTAHProvisioningException(
'initrd file does have have gz or lz extension: {}'
.format(initrd))
pipe.append('cpio -ivd 2>/dev/null', '-.')
exitstatus = pipe.copy(initrd, '/dev/null')
if exitstatus != 0:
# Currently this comes up as 512 when things seem fine
# TODO: refactor this and check for codes at all stages
self.logger.debug(
'Unpacking initrd exited with status {}'.format(exitstatus))
def _setuplatecommand(self, tmpdir=None):
"""Setup the latecommand script we run during a preseeded install."""
# TODO: document this better
self.logger.info('Copying ssh public key')
if tmpdir is None:
tmpdir = self.tmpdir
shutil.copyfile(config.sshpublickey,
os.path.join(tmpdir, 'initrd.d', 'utah-ssh-key'))
self.logger.info('Creating latecommand scripts')
filename = os.path.join(tmpdir, 'initrd.d', 'utah-latecommand')
template.write('utah-latecommand.jinja2',
filename,
user=config.user,
uuid=self.uuid,
log_file='/target/var/log/utah-install',
media_info=self.image.media_info,
install_type=self.image.installtype,
md5=self.image.getmd5())
filename = os.path.join(tmpdir, 'initrd.d', 'utah-setup')
template.write('utah-setup.jinja2',
filename,
packages=config.installpackages,
log_file='/var/log/utah-install')
filename = os.path.join(tmpdir, 'initrd.d', 'utah-autorun.sh')
template.write('utah-autorun.sh.jinja2',
filename,
log_file='/var/log/utah-install')
def _setuppreseed(self, tmpdir=None):
"""Rewrite the preseed to automate installation and access."""
# TODO: document this better
# TODO: if the lines we need aren't in the preseed, add them
self.logger.info('Setting up preseed')
if tmpdir is None:
tmpdir = self.tmpdir
if self.rewrite in ['all', 'minimal']:
with open(self.preseed) as f:
preseed = Preseed(f)
self._rewrite_latecommand(preseed, tmpdir)
if self.rewrite == 'all':
if 'pkgsel/include' in preseed:
self._rewrite_pkgsel_include(preseed)
if 'netcfg/get_hostname' in preseed:
self._rewrite_get_hostname(preseed)
if 'passwd/username' in preseed:
self._rewrite_passwd_username(preseed)
if self.image.installtype == 'desktop':
self._rewrite_failure_command(preseed)
output_preseed_filename = os.path.join(tmpdir,
'initrd.d', 'preseed.cfg')
with open(output_preseed_filename, 'w') as f:
f.write(preseed.dump())
self.finalpreseed = output_preseed_filename
else:
self.logger.info('Not altering preseed because rewrite is %s',
self.rewrite)
if (self.image.installtype == 'desktop' and
self.rewrite in ['all', 'minimal', 'casperonly']):
self._preseedcasper(tmpdir=tmpdir)
def _rewrite_latecommand(self, preseed, tmpdir):
"""Rewrite latecommand in preseed."""
# Create a late command question if not present already
if not 'preseed/late_command' in preseed:
preseed.append('d-i preseed/late_command string')
question = preseed['preseed/late_command']
log_file = '/var/log/utah-install'
if self.image.installtype == 'desktop':
self.logger.info('Changing d-i latecommand '
'to ubiquity success_command '
'and prepending ubiquity lines')
question.owner = 'ubiquity'
question.qname = 'ubiquity/success_command'
question.prepend('ubiquity ubiquity/summary note')
question.prepend('ubiquity ubiquity/reboot boolean true')
filename = os.path.join(tmpdir, 'initrd.d', 'latecommand-wrapper')
target_log_file = '/target{}'.format(log_file)
template.write('latecommand-wrapper.jinja2',
filename,
latecommand=question.value.text,
log_file=target_log_file)
question.value = (
'sh latecommand-wrapper || '
'logger -s -t utah "Late command failure detected" '
'2>>{}'.format(target_log_file))
def _rewrite_failure_command(self, preseed):
"""Add log message to failure command.
When an ubiquity installation fails, ubiquity/failure_command is
called. This is a nice place to write a log message, so that when
troubleshotting a provisioing problem it's clearly stated that was the
image instalation itself that failed.
:param preseed: The preseed contents before is written to initrd.d
:type preseed: :class:`Preseed`
.. note::
If ubiquity/failure_command question isn't present in the preseed,
a new entry will be created for it.
"""
# Create an ubiquity/failure_command question if not present already
command = 'logger -t utah "Installation failure detected"'
if not 'ubiquity/failure_command' in preseed:
self.logger.debug(
'Adding ubiquity/failure_command question with log messsage')
preseed.append('ubiquity ubiquity/failure_command string {}'
.format(command))
else:
self.logger.debug(
'Adding log message to ubiquity/failure_command question')
question = preseed['ubiquity/failure_command']
question.value.prepend('{}; '.format(command))
def _rewrite_pkgsel_include(self, preseed):
"""Add packages required by utah client to pkgsel/include.
Only works for debian-installer
"""
question = preseed['pkgsel/include']
packages = question.value.text.split()
for pkgname in config.installpackages:
if pkgname not in packages:
self.logger.info('Adding {} to preseeded packages'
.format(pkgname))
packages.append(pkgname)
question.value = ' '.join(packages)
def _rewrite_get_hostname(self, preseed):
"""Set hostname in the preseed."""
self.logger.info('Rewriting hostname to %s', self.name)
question = preseed['netcfg/get_hostname']
question.value = self.name
def _rewrite_passwd_username(self, preseed):
"""Set password in the preseed."""
self.logger.info('Rewriting username to %s', config.user)
question = preseed['passwd/username']
question.value = config.user
def _preseedcasper(self, tmpdir=None):
"""Insert a preseed file into casper."""
self.logger.info('Inserting preseed into casper')
if tmpdir is None:
tmpdir = self.tmpdir
casper_dir = os.path.join(tmpdir, 'initrd.d', 'scripts',
'casper-bottom')
filename = os.path.join(casper_dir, 'utah')
template.write('casper-preseed-script.jinja2', filename)
os.chmod(filename, 0755)
orderfilename = os.path.join(casper_dir, 'ORDER')
exitstatus = ProcessRunner(
['sed', '-i',
'1i/scripts/casper-bottom/'
'utah\\n'
'[ -e /conf/param.conf ] && . /conf/param.conf',
orderfilename]).returncode
if exitstatus != 0:
raise UTAHProvisioningException('Failed to setup casper script')
casper_file = os.path.join(tmpdir, 'initrd.d', 'etc', 'casper.conf')
tmpfilename = '{}.tmp'.format(casper_file)
with open(casper_file, 'r') as i:
with open(tmpfilename, 'w') as o:
for line in i:
if 'export HOST' in line:
o.write('export HOST="{}"\n'.format(self.name))
elif 'export FLAVOUR' in line:
o.write('export FLAVOUR="Ubuntu"\n')
else:
o.write(line)
o.flush()
os.rename(tmpfilename, casper_file)
def _setuplogging(self, tmpdir=None):
"""Route the installer syslog.
For virtual machines, this goes to a serial console that libvirt
redirects to a local file we can read.
"""
if tmpdir is None:
tmpdir = self.tmpdir
inittab = os.path.join(tmpdir, 'initrd.d', 'etc', 'inittab')
if os.path.isfile(inittab):
self.logger.info('Updating inittab')
with open(inittab, 'a') as myfile:
myfile.write("\n"
"# logging to serial\n"
"ttyS0::respawn:/usr/bin/tail -n 1200 "
"-f /var/log/syslog \n")
self.logger.info('Creating rsyslog config file')
conffilename = os.path.join(tmpdir, 'initrd.d', '50-utahdefault.conf')
with open(conffilename, 'w') as f:
if self.rsyslog.port:
ip = self._ipaddr(config.bridge)
dest = '@{}:{}'.format(ip, self.rsyslog.port)
else:
self.logger.debug('setting up logging to go to serial console')
dest = '|/dev/ttyS0'
f.write(template.as_buff('50-utahdefault.conf.jinja2', dest=dest))
def _repackinitrd(self, tmpdir=None):
"""Pack an initrd from our directory.
:returns: The path to the initrd file we packed.
:rtype: str
"""
self.logger.info('Repacking initrd')
if tmpdir is None:
tmpdir = self.tmpdir
pipe = pipes.Template()
pipe.prepend('find .', '.-')
pipe.append('cpio --quiet -o -H newc', '--')
# Desktop image loads initrd.gz,
# but for physical machines we should stick with lz
if self.image.installtype == 'desktop':
self.logger.debug('Using lzma because installtype is desktop')
pipe.append('lzma -9fc ', '--')
initrd = os.path.join(tmpdir, 'initrd.lz')
else:
self.logger.debug('Using gzip because installtype is not desktop')
pipe.append('gzip -9fc ', '--')
initrd = os.path.join(tmpdir, 'initrd.gz')
if pipe.copy('/dev/null', initrd) != 0:
raise UTAHProvisioningException('Failed to repack initrd')
return initrd
def _cmdlinesetup(self):
"""Setup the command line for an unattended install.
If any options known to be needed for an automated install are not
present, add them.
:param boot: Manually specified kernel command line.
:type boot: str
"""
# TODO: update libvirtvm to work like the hardware provisioners
# or vice versa
self.cmdline = self.boot or ''
# TODO: Refactor this into lists like BambooFeederMachine
if self.rewrite == 'all':
self.logger.info('Adding needed command line options')
options = []
parameters = [
('netcfg/get_hostname', self.name),
('log_host', self._ipaddr(config.bridge)),
('log_port', str(self.rsyslog.port)),
]
if self.image.installtype == 'desktop':
options.extend([
'automatic-ubiquity',
'noprompt',
])
parameters.extend([
('boot', 'casper'),
('keyboard-configuration/layoutcode', 'us'),
])
else:
parameters.extend([
('DEBCONF_DEBUG', 'developer'),
('debconf/priority', 'critical'),
])
for option in tuple(options):
if option not in self.cmdline:
self.logger.info('Adding boot option: %s', option)
self.cmdline = '{} {}'.format(self.cmdline, option)
for parameter in tuple(parameters):
if parameter[0] not in self.cmdline:
self.logger.info('Adding boot option: %s',
'='.join(parameter))
self.cmdline = ('{} {}'
.format(self.cmdline,
'='.join(parameter)))
self.cmdline = self.cmdline.strip()
else:
self.logger.info('Not altering command line since rewrite is %s',
self.rewrite)
self.logger.info('Boot command line is: {}'.format(self.cmdline))
@staticmethod
def _ipaddr(ifname):
"""Return the first IP address found for the given interface name.
:param ifname: Name of the network interface
:type ifname: str
"""
iface = netifaces.ifaddresses(ifname)
return iface[netifaces.AF_INET][0]['addr']