summaryrefslogtreecommitdiff
path: root/morphlib
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2013-02-12 11:51:52 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2013-02-12 11:51:52 +0000
commitc00ee1c6852d5d02cd9d9fdf071b9a3d838ad6ef (patch)
tree38b4ff8a91b977bb04c700a0e986c21c436266f3 /morphlib
parentd83af40a3cdff4905af0e41c60a96744078a4b52 (diff)
parenta9ec7e0bdf6b6dc9b15addbe9980f3b03fe342ea (diff)
downloadmorph-c00ee1c6852d5d02cd9d9fdf071b9a3d838ad6ef.tar.gz
Merge branch 'liw/deployment-refactor' of git://git.baserock.org/baserock/baserock/morph
Diffstat (limited to 'morphlib')
-rw-r--r--morphlib/__init__.py2
-rwxr-xr-xmorphlib/exts/kvm.write110
-rwxr-xr-xmorphlib/exts/rawdisk.write49
-rwxr-xr-xmorphlib/exts/set-hostname.configure27
-rwxr-xr-xmorphlib/exts/virtualbox-ssh.write129
-rw-r--r--morphlib/morph2.py3
-rw-r--r--morphlib/plugins/__init__.py0
-rw-r--r--morphlib/plugins/branch_and_merge_plugin.py1
-rw-r--r--morphlib/plugins/deploy_plugin.py209
-rwxr-xr-xmorphlib/writeexts.py185
10 files changed, 714 insertions, 1 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
index 0f60642c..4446720e 100644
--- a/morphlib/__init__.py
+++ b/morphlib/__init__.py
@@ -73,4 +73,6 @@ import util
import yamlparse
+import writeexts
+
import app # this needs to be last
diff --git a/morphlib/exts/kvm.write b/morphlib/exts/kvm.write
new file mode 100755
index 00000000..09a7d224
--- /dev/null
+++ b/morphlib/exts/kvm.write
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+# Copyright (C) 2012-2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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.
+
+
+'''A Morph deployment write extension for deploying to KVM+libvirt.'''
+
+
+import cliapp
+import os
+import re
+import sys
+import tempfile
+import urlparse
+
+import morphlib.writeexts
+
+
+class KvmPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
+
+ '''Create a KVM/LibVirt virtual machine during Morph's deployment.
+
+ The location command line argument is the pathname of the disk image
+ to be created. The user is expected to provide the location argument
+ using the following syntax:
+
+ kvm+ssh://HOST/GUEST/PATH
+
+ where:
+
+ * HOST is the host on which KVM/LibVirt is running
+ * GUEST is the name of the guest virtual machine on that host
+ * PATH is the path to the disk image that should be created,
+ on that host
+
+ The extension will connect to HOST via ssh to run libvirt's
+ command line management tools.
+
+ '''
+
+ def process_args(self, args):
+ if len(args) != 2:
+ raise cliapp.AppException('Wrong number of command line args')
+
+ temp_root, location = args
+ ssh_host, vm_name, vm_path = self.parse_location(location)
+
+ fd, raw_disk = tempfile.mkstemp()
+ os.close(fd)
+ self.create_local_system(temp_root, raw_disk)
+
+ try:
+ self.transfer(raw_disk, ssh_host, vm_path)
+ self.create_libvirt_guest(ssh_host, vm_name, vm_path)
+ except BaseException:
+ sys.stderr.write('Error deploying to libvirt')
+ os.remove(raw_disk)
+ raise
+ else:
+ os.remove(raw_disk)
+
+ self.status(
+ msg='Virtual machine %(vm_name)s has been created',
+ vm_name=vm_name)
+
+ def parse_location(self, location):
+ '''Parse the location argument to get relevant data.'''
+
+ x = urlparse.urlparse(location)
+ if x.scheme != 'kvm+ssh':
+ raise cliapp.AppException(
+ 'URL schema must be vbox+ssh in %s' % location)
+ m = re.match('^/(?P<guest>[^/]+)(?P<path>/.+)$', x.path)
+ if not m:
+ raise cliapp.AppException('Cannot parse location %s' % location)
+ return x.netloc, m.group('guest'), m.group('path')
+
+ def transfer(self, raw_disk, ssh_host, vm_path):
+ '''Transfer raw disk image to libvirt host.'''
+
+ self.status(msg='Transfer disk image')
+ target = '%s:%s' % (ssh_host, vm_path)
+ with open(raw_disk, 'rb') as f:
+ cliapp.runcmd(['rsync', '-zS', raw_disk, target])
+
+ def create_libvirt_guest(self, ssh_host, vm_name, vm_path):
+ '''Create the libvirt virtual machine.'''
+
+ self.status(msg='Create libvirt/kvm virtual machine')
+ cliapp.runcmd(
+ ['ssh', ssh_host,
+ 'virt-install', '--connect qemu:///system', '--import',
+ '--name', vm_name, '--ram', '1024', '--vnc', '--noreboot',
+ '--disk path=%s,bus=ide' % vm_path])
+
+
+KvmPlusSshWriteExtension().run()
+
diff --git a/morphlib/exts/rawdisk.write b/morphlib/exts/rawdisk.write
new file mode 100755
index 00000000..a55473f2
--- /dev/null
+++ b/morphlib/exts/rawdisk.write
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+# Copyright (C) 2012-2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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.
+
+
+'''A Morph deployment write extension for raw disk images.'''
+
+
+import os
+import sys
+import time
+import tempfile
+
+import morphlib.writeexts
+
+
+class RawDiskWriteExtension(morphlib.writeexts.WriteExtension):
+
+ '''Create a raw disk image during Morph's deployment.
+
+ The location command line argument is the pathname of the disk image
+ to be created.
+
+ '''
+
+ def process_args(self, args):
+ if len(args) != 2:
+ raise cliapp.AppException('Wrong number of command line args')
+
+ temp_root, location = args
+
+ self.create_local_system(temp_root, location)
+ self.status(msg='Disk image has been created at %s' % location)
+
+
+RawDiskWriteExtension().run()
+
diff --git a/morphlib/exts/set-hostname.configure b/morphlib/exts/set-hostname.configure
new file mode 100755
index 00000000..e44c5d56
--- /dev/null
+++ b/morphlib/exts/set-hostname.configure
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Copyright (C) 2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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.
+
+
+# Set hostname on system from HOSTNAME.
+
+
+set -e
+
+if [ -n "$HOSTNAME" ]
+then
+ echo "$HOSTNAME" > "$1/etc/hostname"
+fi
+
diff --git a/morphlib/exts/virtualbox-ssh.write b/morphlib/exts/virtualbox-ssh.write
new file mode 100755
index 00000000..c21dcc57
--- /dev/null
+++ b/morphlib/exts/virtualbox-ssh.write
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+# Copyright (C) 2012-2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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.
+
+
+'''A Morph deployment write extension for deploying to VirtualBox via ssh.
+
+VirtualBox is assumed to be running on a remote machine, which is
+accessed over ssh. The machine gets created, but not started.
+
+'''
+
+
+import cliapp
+import os
+import re
+import time
+import tempfile
+import urlparse
+
+import morphlib.writeexts
+
+
+class VirtualBoxPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
+
+ '''Create a VirtualBox virtual machine during Morph's deployment.
+
+ The location command line argument is the pathname of the disk image
+ to be created. The user is expected to provide the location argument
+ using the following syntax:
+
+ vbox+ssh://HOST/GUEST/PATH
+
+ where:
+
+ * HOST is the host on which VirtualBox is running
+ * GUEST is the name of the guest virtual machine on that host
+ * PATH is the path to the disk image that should be created,
+ on that host
+
+ The extension will connect to HOST via ssh to run VirtualBox's
+ command line management tools.
+
+ '''
+
+ def process_args(self, args):
+ if len(args) != 2:
+ raise cliapp.AppException('Wrong number of command line args')
+
+ temp_root, location = args
+ ssh_host, vm_name, vdi_path = self.parse_location(location)
+
+ fd, raw_disk = tempfile.mkstemp()
+ os.close(fd)
+ self.create_local_system(temp_root, raw_disk)
+
+ try:
+ self.transfer_and_convert_to_vdi(
+ raw_disk, size, ssh_host, vdi_path)
+ self.create_virtualbox_guest(ssh_host, vm_name, vdi_path)
+ except BaseException:
+ sys.stderr.write('Error deploying to VirtualBox')
+ os.remove(raw_disk)
+ raise
+ else:
+ os.remove(raw_disk)
+
+ self.status(
+ msg='Virtual machine %(vm_name)s has been created',
+ vm_name=vm_name)
+
+ def parse_location(self, location):
+ '''Parse the location argument to get relevant data.'''
+
+ x = urlparse.urlparse(location)
+ if x.scheme != 'vbox+ssh':
+ raise cliapp.AppException(
+ 'URL schema must be vbox+ssh in %s' % location)
+ m = re.match('^/(?P<guest>[^/]+)(?P<path>/.+)$', x.path)
+ if not m:
+ raise cliapp.AppException('Cannot parse location %s' % location)
+ return x.netloc, m.group('guest'), m.group('path')
+
+ def transfer_and_convert_to_vdi(self, raw_disk, size, ssh_host, vdi_path):
+ '''Transfer raw disk image to VirtualBox host, and convert to VDI.'''
+
+ self.status(msg='Transfer disk and convert to VDI')
+ with open(raw_disk, 'rb') as f:
+ cliapp.runcmd(
+ ['ssh', ssh_host,
+ 'VBoxManage', 'convertfromraw', 'stdin', vdi_path, str(size)],
+ stdin=f)
+
+ def create_virtualbox_guest(self, ssh_host, vm_name, vdi_path):
+ '''Create the VirtualBox virtual machine.'''
+
+ self.status(msg='Create VirtualBox virtual machine')
+
+ commands = [
+ ['createvm', '--name', vm_name, '--ostype', 'Linux26_64',
+ '--register'],
+ ['modifyvm', vm_name, '--ioapic', 'on', '--memory', '1024',
+ '--nic1', 'nat'],
+ ['storagectl', vm_name, '--name', '"SATA Controller"',
+ '--add', 'sata', '--bootable', 'on', '--sataportcount', '2'],
+ ['storageattach', vm_name, '--storagectl', '"SATA Controller"',
+ '--port', '0', '--device', '0', '--type', 'hdd', '--medium',
+ vdi_path],
+ ]
+
+ for command in commands:
+ argv = ['ssh', ssh_host, 'VBoxManage'] + command
+ cliapp.runcmd(argv)
+
+
+VirtualBoxPlusSshWriteExtension().run()
+
diff --git a/morphlib/morph2.py b/morphlib/morph2.py
index 3a3ad679..6e24765e 100644
--- a/morphlib/morph2.py
+++ b/morphlib/morph2.py
@@ -49,7 +49,8 @@ class Morphology(object):
('strata', []),
('description', ''),
('arch', None),
- ('system-kind', None)
+ ('system-kind', None),
+ ('configuration-extensions', []),
]
}
diff --git a/morphlib/plugins/__init__.py b/morphlib/plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/morphlib/plugins/__init__.py
diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py
index 58aa8931..0f07c431 100644
--- a/morphlib/plugins/branch_and_merge_plugin.py
+++ b/morphlib/plugins/branch_and_merge_plugin.py
@@ -331,6 +331,7 @@ class BranchAndMergePlugin(cliapp.Plugin):
'description',
'disk-size',
'_disk-size',
+ 'configuration-extensions',
],
'stratum': [
'kind',
diff --git a/morphlib/plugins/deploy_plugin.py b/morphlib/plugins/deploy_plugin.py
new file mode 100644
index 00000000..79715e13
--- /dev/null
+++ b/morphlib/plugins/deploy_plugin.py
@@ -0,0 +1,209 @@
+# Copyright (C) 2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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 cliapp
+import gzip
+import os
+import shutil
+import tarfile
+import tempfile
+import urlparse
+import uuid
+
+import morphlib
+
+# UGLY HACK: We need to re-use some code from the branch and merge
+# plugin, so we import and instantiate that plugin. This needs to
+# be fixed by refactoring the codebase so the shared code is in
+# morphlib, not in a plugin. However, this hack lets us re-use
+# code without copying it.
+import morphlib.plugins.branch_and_merge_plugin
+
+
+class DeployPlugin(cliapp.Plugin):
+
+ def enable(self):
+ self.app.add_subcommand(
+ 'deploy', self.deploy,
+ arg_synopsis='TYPE SYSTEM LOCATION [KEY=VALUE]')
+ self.other = \
+ morphlib.plugins.branch_and_merge_plugin.BranchAndMergePlugin()
+ self.other.app = self.app
+
+ def disable(self):
+ pass
+
+ def deploy(self, args):
+ '''Deploy a built system image.'''
+
+ if len(args) < 3:
+ raise cliapp.AppException(
+ 'Too few arguments to deploy command (see help)')
+
+ deployment_type = args[0]
+ system_name = args[1]
+ location = args[2]
+ env_vars = args[3:]
+
+ # Deduce workspace and system branch and branch root repository.
+ workspace = self.other.deduce_workspace()
+ branch, branch_dir = self.other.deduce_system_branch()
+ branch_root = self.other.get_branch_config(branch_dir, 'branch.root')
+ branch_uuid = self.other.get_branch_config(branch_dir, 'branch.uuid')
+
+ # Generate a UUID for the build.
+ build_uuid = uuid.uuid4().hex
+
+ build_command = morphlib.buildcommand.BuildCommand(self.app)
+ build_command = self.app.hookmgr.call('new-build-command',
+ build_command)
+ push = self.app.settings['push-build-branches']
+
+ self.app.status(msg='Starting build %(uuid)s', uuid=build_uuid)
+
+ self.app.status(msg='Collecting morphologies involved in '
+ 'building %(system)s from %(branch)s',
+ system=system_name, branch=branch)
+
+ # Get repositories of morphologies involved in building this system
+ # from the current system branch.
+ build_repos = self.other.get_system_build_repos(
+ branch, branch_dir, branch_root, system_name)
+
+ # Generate temporary build ref names for all these repositories.
+ self.other.generate_build_ref_names(build_repos, branch_uuid)
+
+ # Create the build refs for all these repositories and commit
+ # all uncommitted changes to them, updating all references
+ # to system branch refs to point to the build refs instead.
+ self.other.update_build_refs(build_repos, branch, build_uuid, push)
+
+ if push:
+ self.other.push_build_refs(build_repos)
+ build_branch_root = branch_root
+ else:
+ dirname = build_repos[branch_root]['dirname']
+ build_branch_root = urlparse.urljoin('file://', dirname)
+
+ # Run the build.
+ build_ref = build_repos[branch_root]['build-ref']
+ order = build_command.compute_build_order(
+ build_branch_root,
+ build_ref,
+ system_name + '.morph')
+ artifact = order.groups[-1][-1]
+
+ if push:
+ self.other.delete_remote_build_refs(build_repos)
+
+ # Unpack the artifact (tarball) to a temporary directory.
+ self.app.status(msg='Unpacking system for configuration')
+
+ system_tree = tempfile.mkdtemp()
+
+ if build_command.lac.has(artifact):
+ f = build_command.lac.get(artifact)
+ else:
+ f = build_command.rac.get(artifact)
+ ff = gzip.GzipFile(fileobj=f)
+ tf = tarfile.TarFile(fileobj=ff)
+ tf.extractall(path=system_tree)
+
+ self.app.status(
+ msg='System unpacked at %(system_tree)s',
+ system_tree=system_tree)
+
+ # Set up environment for running extensions.
+ env = dict(os.environ)
+ for spec in env_vars:
+ name, value = spec.split('=', 1)
+ if name in env:
+ raise morphlib.Error(
+ '%s is already set in the enviroment' % name)
+ env[name] = value
+
+ # Run configuration extensions.
+ self.app.status(msg='Configure system')
+ names = artifact.source.morphology['configuration-extensions']
+ for name in names:
+ self._run_extension(
+ branch_dir,
+ build_ref,
+ name,
+ '.configure',
+ [system_tree],
+ env)
+
+ # Run write extension.
+ self.app.status(msg='Writing to device')
+ self._run_extension(
+ branch_dir,
+ build_ref,
+ deployment_type,
+ '.write',
+ [system_tree, location],
+ env)
+
+ # Cleanup.
+ self.app.status(msg='Cleaning up')
+ shutil.rmtree(system_tree)
+
+ self.app.status(msg='Finished deployment')
+
+ def _run_extension(self, repo_dir, ref, name, kind, args, env):
+ '''Run an extension.
+
+ The ``kind`` should be either ``.configure`` or ``.write``,
+ depending on the kind of extension that is sought.
+
+ The extension is found either in the git repository of the
+ system morphology (repo, ref), or with the Morph code.
+
+ '''
+
+ # Look for extension in the system morphology's repository.
+ ext = self._cat_file(repo_dir, ref, name + kind)
+ if ext is None:
+ # Not found: look for it in the Morph code.
+ code_dir = os.path.dirname(morphlib.__file__)
+ ext_filename = os.path.join(code_dir, 'exts', name + kind)
+ if not os.path.exists(ext_filename):
+ raise morphlib.Error(
+ 'Could not find extension %s%s' % (name, kind))
+ delete_ext = False
+ else:
+ # Found it in the system morphology's repository.
+ fd, ext_filename = tempfile.mkstemp()
+ os.write(fd, ext)
+ os.close(fd)
+ os.chmod(ext_filename, 0700)
+ delete_ext = True
+
+ self.app.runcmd(
+ [ext_filename] + args, env=env, stdout=None, stderr=None)
+
+ if delete_ext:
+ os.remove(ext_filename)
+
+ def _cat_file(self, repo_dir, ref, pathname):
+ '''Retrieve contents of a file from a git repository.'''
+
+ argv = ['git', 'cat-file', 'blob', '%s:%s' % (ref, pathname)]
+ try:
+ return self.app.runcmd(argv, cwd=repo_dir)
+ except cliapp.AppException:
+ return None
+
diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py
new file mode 100755
index 00000000..60848345
--- /dev/null
+++ b/morphlib/writeexts.py
@@ -0,0 +1,185 @@
+# Copyright (C) 2012-2013 Codethink Limited
+#
+# 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; version 2 of the License.
+#
+# 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 cliapp
+import os
+import re
+import time
+import tempfile
+
+
+class WriteExtension(cliapp.Application):
+
+ '''A base class for deployment write extensions.
+
+ A subclass should subclass this class, and add a
+ ``process_args`` method.
+
+ Note that it is not necessary to subclass this class for write
+ extensions. This class is here just to collect common code for
+ write extensions.
+
+ '''
+
+ def process_args(self, args):
+ raise NotImplementedError()
+
+ def status(self, **kwargs):
+ '''Provide status output.
+
+ The ``msg`` keyword argument is the actual message,
+ the rest are values for fields in the message as interpolated
+ by %.
+
+ '''
+
+ self.output.write('%s\n' % (kwargs['msg'] % kwargs))
+
+ def create_local_system(self, temp_root, raw_disk):
+ '''Create a raw system image locally.'''
+
+ size = self.get_disk_size()
+ self.create_raw_disk_image(raw_disk, size)
+ try:
+ self.mkfs_btrfs(raw_disk)
+ mp = self.mount(raw_disk)
+ except BaseException:
+ sys.stderr.write('Error creating disk image')
+ os.remove(raw_disk)
+ raise
+ try:
+ self.create_factory(mp, temp_root)
+ self.create_fstab(mp)
+ self.create_factory_run(mp)
+ self.install_extlinux(mp)
+ except BaseException, e:
+ sys.stderr.write('Error creating disk image')
+ self.unmount(mp)
+ os.remove(raw_disk)
+ raise
+ else:
+ self.unmount(mp)
+
+ def get_disk_size(self):
+ '''Parse disk size from environment.'''
+
+ size = os.environ.get('DISK_SIZE', '1G')
+ m = re.match('^(\d+)([kmgKMG]?)$', size)
+ if not m:
+ raise morphlib.Error('Cannot parse disk size %s' % size)
+
+ factors = {
+ '': 1,
+ 'k': 1024,
+ 'm': 1024**2,
+ 'g': 1024**3,
+ }
+ factor = factors[m.group(2).lower()]
+
+ return int(m.group(1)) * factor
+
+ def create_raw_disk_image(self, filename, size):
+ '''Create a raw disk image.'''
+
+ self.status(msg='Creating empty disk image')
+ with open(filename, 'wb') as f:
+ if size > 0:
+ f.seek(size-1)
+ f.write('\0')
+
+ def mkfs_btrfs(self, location):
+ '''Create a btrfs filesystem on the disk.'''
+ self.status(msg='Creating btrfs filesystem')
+ cliapp.runcmd(['mkfs.btrfs', '-L', 'baserock', location])
+
+ def mount(self, location):
+ '''Mount the filesystem so it can be tweaked.
+
+ Return path to the mount point.
+ The mount point is a newly created temporary directory.
+ The caller must call self.unmount to unmount on the return value.
+
+ '''
+
+ self.status(msg='Mounting filesystem')
+ tempdir = tempfile.mkdtemp()
+ cliapp.runcmd(['mount', '-o', 'loop', location, tempdir])
+ return tempdir
+
+ def unmount(self, mount_point):
+ '''Unmount the filesystem mounted by self.mount.
+
+ Also, remove the temporary directory.
+
+ '''
+
+ self.status(msg='Unmounting filesystem')
+ cliapp.runcmd(['umount', mount_point])
+ os.rmdir(mount_point)
+
+ def create_factory(self, real_root, temp_root):
+ '''Create the default "factory" system.'''
+
+ factory = os.path.join(real_root, 'factory')
+
+ self.status(msg='Creating factory subvolume')
+ cliapp.runcmd(['btrfs', 'subvolume', 'create', factory])
+ self.status(msg='Copying files to factory subvolume')
+ cliapp.runcmd(['cp', '-a', temp_root + '/.', factory + '/.'])
+
+ # The kernel needs to be on the root volume.
+ self.status(msg='Copying boot directory to root subvolume')
+ factory_boot = os.path.join(factory, 'boot')
+ root_boot = os.path.join(real_root, 'boot')
+ cliapp.runcmd(['cp', '-a', factory_boot, root_boot])
+
+ def create_factory_run(self, real_root):
+ '''Create the 'factory-run' snapshot.'''
+
+ self.status(msg='Creating factory-run subvolume')
+ factory = os.path.join(real_root, 'factory')
+ factory_run = factory + '-run'
+ cliapp.runcmd(
+ ['btrfs', 'subvolume', 'snapshot', factory, factory_run])
+
+ def create_fstab(self, real_root):
+ '''Create an fstab.'''
+
+ self.status(msg='Creating fstab')
+ fstab = os.path.join(real_root, 'factory', 'etc', 'fstab')
+ with open(fstab, 'w') as f:
+ f.write('/dev/sda / btrfs defaults,rw,noatime 0 1\n')
+
+ def install_extlinux(self, real_root):
+ '''Install extlinux on the newly created disk image.'''
+
+ self.status(msg='Creating extlinux.conf')
+ config = os.path.join(real_root, 'extlinux.conf')
+ with open(config, 'w') as f:
+ f.write('default linux\n')
+ f.write('timeout 1\n')
+ f.write('label linux\n')
+ f.write('kernel /boot/vmlinuz\n')
+ f.write('append root=/dev/sda rootflags=subvol=factory-run '
+ 'init=/sbin/init rw\n')
+
+ self.status(msg='Installing extlinux')
+ cliapp.runcmd(['extlinux', '--install', real_root])
+
+ # FIXME this hack seems to be necessary to let extlinux finish
+ cliapp.runcmd(['sync'])
+ time.sleep(2)
+