From ce12519daf240462e89bc01e8263a030c3465db4 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 6 Feb 2013 20:44:47 +0000 Subject: Add morphlib module for common write extension code --- morphlib/writeexts.py | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100755 morphlib/writeexts.py (limited to 'morphlib/writeexts.py') diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py new file mode 100755 index 00000000..23473021 --- /dev/null +++ b/morphlib/writeexts.py @@ -0,0 +1,157 @@ +# 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 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') + cliapp.runcmd( + ['dd', + 'if=/dev/zero', + 'of=' + filename, + 'bs=1', + 'seek=%d' % size, + 'count=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() + # FIXME: This hardcodes the loop device. + cliapp.runcmd(['mount', '-o', 'loop=loop0', 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_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('proc /proc proc defaults 0 0\n') + f.write('sysfs /sys sysfs defaults 0 0\n') + 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 ' + '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) + -- cgit v1.2.1 From 4a2578e39c8074bf9fb75870a045da88ffd6e299 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 7 Feb 2013 11:27:52 +0000 Subject: Create hole in-process without executing dd(1) Suggested-By: Richard Maw --- morphlib/writeexts.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'morphlib/writeexts.py') diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py index 23473021..676a9d22 100755 --- a/morphlib/writeexts.py +++ b/morphlib/writeexts.py @@ -70,13 +70,10 @@ class WriteExtension(cliapp.Application): '''Create a raw disk image.''' self.status(msg='Creating empty disk image') - cliapp.runcmd( - ['dd', - 'if=/dev/zero', - 'of=' + filename, - 'bs=1', - 'seek=%d' % size, - 'count=0']) + 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.''' -- cgit v1.2.1 From 46e7e99c2b56766a3e858c7cb1584937a748a572 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 7 Feb 2013 11:29:57 +0000 Subject: Let mount choose loop device Suggested-By: Richard Maw --- morphlib/writeexts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'morphlib/writeexts.py') diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py index 676a9d22..a407c059 100755 --- a/morphlib/writeexts.py +++ b/morphlib/writeexts.py @@ -91,8 +91,7 @@ class WriteExtension(cliapp.Application): self.status(msg='Mounting filesystem') tempdir = tempfile.mkdtemp() - # FIXME: This hardcodes the loop device. - cliapp.runcmd(['mount', '-o', 'loop=loop0', location, tempdir]) + cliapp.runcmd(['mount', '-o', 'loop', location, tempdir]) return tempdir def unmount(self, mount_point): -- cgit v1.2.1 From e89cdd46dc1cf9ccb17e4d57fb212445481ec855 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 7 Feb 2013 11:45:19 +0000 Subject: Do away with unnecessary fstab entries for proc, sys Suggested-By: Richard Maw --- morphlib/writeexts.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'morphlib/writeexts.py') diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py index a407c059..fae0d5d9 100755 --- a/morphlib/writeexts.py +++ b/morphlib/writeexts.py @@ -127,8 +127,6 @@ class WriteExtension(cliapp.Application): self.status(msg='Creating fstab') fstab = os.path.join(real_root, 'factory', 'etc', 'fstab') with open(fstab, 'w') as f: - f.write('proc /proc proc defaults 0 0\n') - f.write('sysfs /sys sysfs defaults 0 0\n') f.write('/dev/sda / btrfs defaults,rw,noatime 0 1\n') def install_extlinux(self, real_root): -- cgit v1.2.1 From 794301e1e1ec1b35145ae7bdd9093909c6488478 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 7 Feb 2013 11:41:04 +0000 Subject: Refactor: Add WriteExtension.create_local_system method This allows code sharing amongst all the places that create a system in a raw disk image. This also adds the creation of a factory-run subvolume, and fixes error messages for errors that happen during a disk image creation. Suggested-By: Richard Maw Suggested-By: Sam Thursfield --- morphlib/writeexts.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'morphlib/writeexts.py') diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py index fae0d5d9..60848345 100755 --- a/morphlib/writeexts.py +++ b/morphlib/writeexts.py @@ -47,6 +47,31 @@ class WriteExtension(cliapp.Application): ''' 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.''' @@ -120,6 +145,15 @@ class WriteExtension(cliapp.Application): 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.''' @@ -139,7 +173,7 @@ class WriteExtension(cliapp.Application): 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 ' + f.write('append root=/dev/sda rootflags=subvol=factory-run ' 'init=/sbin/init rw\n') self.status(msg='Installing extlinux') -- cgit v1.2.1