summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xmorphlib/exts/kvm.write7
-rwxr-xr-xmorphlib/exts/nfsboot.write22
-rwxr-xr-xmorphlib/exts/rawdisk.write11
-rwxr-xr-xmorphlib/exts/ssh-rsync.write28
-rwxr-xr-xmorphlib/exts/virtualbox-ssh.write72
-rw-r--r--morphlib/localartifactcache.py28
-rw-r--r--morphlib/localartifactcache_tests.py36
-rw-r--r--morphlib/plugins/gc_plugin.py158
-rw-r--r--morphlib/util.py33
-rw-r--r--morphlib/writeexts.py9
-rw-r--r--without-test-modules1
11 files changed, 365 insertions, 40 deletions
diff --git a/morphlib/exts/kvm.write b/morphlib/exts/kvm.write
index f2683d8e..67ac40e7 100755
--- a/morphlib/exts/kvm.write
+++ b/morphlib/exts/kvm.write
@@ -94,7 +94,7 @@ class KvmPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
self.status(msg='Transferring disk image')
target = '%s:%s' % (ssh_host, vm_path)
with open(raw_disk, 'rb') as f:
- cliapp.runcmd(['rsync', '-zS', raw_disk, target])
+ cliapp.runcmd(['rsync', '-szS', raw_disk, target])
def create_libvirt_guest(self, ssh_host, vm_name, vm_path, autostart):
'''Create the libvirt virtual machine.'''
@@ -106,6 +106,11 @@ class KvmPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
for disk in attach_disks:
attach_opts.extend(['--disk', 'path=%s' % disk])
+ if 'NIC_CONFIG' in os.environ:
+ nics = os.environ['NIC_CONFIG'].split()
+ for nic in nics:
+ attach_opts.extend(['--network', nic])
+
ram_mebibytes = str(self.get_ram_size() / (1024**2))
cmdline = ['virt-install', '--connect', 'qemu:///system', '--import',
diff --git a/morphlib/exts/nfsboot.write b/morphlib/exts/nfsboot.write
index 61c5306a..34a72972 100755
--- a/morphlib/exts/nfsboot.write
+++ b/morphlib/exts/nfsboot.write
@@ -66,15 +66,15 @@ class NFSBootWriteExtension(morphlib.writeexts.WriteExtension):
'with hostname "baserock"')
self.test_good_server(location)
- version = os.environ['VERSION'] or 'version1'
+ version_label = os.getenv('VERSION_LABEL', 'factory')
versioned_root = os.path.join(self._nfsboot_root, hostname, 'systems',
- version)
+ version_label)
if self.version_exists(versioned_root, location):
raise cliapp.AppException('Version %s already exists on'
' this device. Deployment aborted'
- % version)
+ % version_label)
self.copy_rootfs(temp_root, location, versioned_root, hostname)
- self.copy_kernel(temp_root, location, versioned_root, version,
+ self.copy_kernel(temp_root, location, versioned_root, version_label,
hostname)
self.configure_nfs(location, hostname)
@@ -118,7 +118,7 @@ class NFSBootWriteExtension(morphlib.writeexts.WriteExtension):
rsync_dest = 'root@%s:%s' % (location, kernel_dest)
self.status(msg='Copying kernel')
cliapp.runcmd(
- ['rsync', kernel_src, rsync_dest])
+ ['rsync', '-s', kernel_src, rsync_dest])
# Link the kernel to the right place
self.status(msg='Creating links to kernel in tftp directory')
@@ -153,7 +153,7 @@ class NFSBootWriteExtension(morphlib.writeexts.WriteExtension):
self.status(msg='Creating \'orig\' rootfs')
cliapp.runcmd(
- ['rsync', '-aXSPH', '--delete', rootfs_src,
+ ['rsync', '-asXSPH', '--delete', rootfs_src,
'root@%s:%s' % (location, orig_path)])
self.status(msg='Creating \'run\' rootfs')
@@ -171,15 +171,15 @@ class NFSBootWriteExtension(morphlib.writeexts.WriteExtension):
raise cliapp.AppException('Could not create \'run\' rootfs'
' from \'orig\'')
- self.status(msg='Linking \'default-run\' to latest system')
+ self.status(msg='Linking \'default\' to latest system')
try:
cliapp.ssh_runcmd('root@%s' % location,
- ['ln', '-sfn', run_path,
+ ['ln', '-sfn', versioned_root,
os.path.join(self._nfsboot_root, hostname, 'systems',
- 'default-run')])
+ 'default')])
except cliapp.AppException:
- raise cliapp.AppException('Could not link \'default-run\' to %s'
- % run_path)
+ raise cliapp.AppException('Could not link \'default\' to %s'
+ % versioned_root)
def configure_nfs(self, location, hostname):
exported_path = os.path.join(self._nfsboot_root, hostname)
diff --git a/morphlib/exts/rawdisk.write b/morphlib/exts/rawdisk.write
index a43a9cce..a74d6905 100755
--- a/morphlib/exts/rawdisk.write
+++ b/morphlib/exts/rawdisk.write
@@ -70,9 +70,18 @@ class RawDiskWriteExtension(morphlib.writeexts.WriteExtension):
self.create_run(version_root)
+ default_path = os.path.join(mp, 'systems', 'default')
+ if os.path.exists(default_path):
+ os.remove(default_path)
+ else:
+ # we are upgrading and old system that does
+ # not have an updated extlinux config file
+ if self.bootloader_is_wanted():
+ self.install_extlinux(mp)
+ os.symlink(version_label, default_path)
+
if self.bootloader_is_wanted():
self.install_kernel(version_root, temp_root)
- self.install_extlinux(mp, version_label)
self.unmount(mp)
diff --git a/morphlib/exts/ssh-rsync.write b/morphlib/exts/ssh-rsync.write
index 6fe1153d..fba550cd 100755
--- a/morphlib/exts/ssh-rsync.write
+++ b/morphlib/exts/ssh-rsync.write
@@ -72,6 +72,21 @@ class SshRsyncWriteExtension(morphlib.writeexts.WriteExtension):
'snapshot', orig_dir, run_dir])
self.install_remote_kernel(location, version_root, temp_root)
+ default_path = os.path.join(remote_mnt, 'systems', 'default')
+ if self.bootloader_is_wanted():
+ output = cliapp.ssh_runcmd(location, ['sh', '-c',
+ 'test -e "$1" && stat -c %F "$1"'
+ ' || '
+ 'echo missing file',
+ '-', default_path])
+ if output != "symbolic link":
+ # we are upgrading and old system that does
+ # not have an updated extlinux config file
+ self.update_remote_extlinux(location, remote_mnt,
+ version_label)
+ cliapp.ssh_runcmd(location, ['ln', '-s', '-f',
+ version_label,
+ default_path])
except Exception as e:
try:
cliapp.ssh_runcmd(location,
@@ -83,9 +98,6 @@ class SshRsyncWriteExtension(morphlib.writeexts.WriteExtension):
pass
raise e
- if self.bootloader_is_wanted():
- self.update_remote_extlinux(location, remote_mnt,
- version_label)
except:
raise
else:
@@ -103,15 +115,15 @@ class SshRsyncWriteExtension(morphlib.writeexts.WriteExtension):
f.write('default linux\n')
f.write('timeout 1\n')
f.write('label linux\n')
- f.write('kernel /systems/' + version_label + '/kernel\n')
+ f.write('kernel /systems/default/kernel\n')
f.write('append root=/dev/sda '
- 'rootflags=subvol=systems/' + version_label + '/run '
+ 'rootflags=subvol=systems/default/run '
'init=/sbin/init rw\n')
cliapp.ssh_runcmd(location, ['mv', config, config+'~'])
try:
- cliapp.runcmd(['rsync', '-a', temp_file,
+ cliapp.runcmd(['rsync', '-as', temp_file,
'%s:%s' % (location, config)])
except Exception as e:
try:
@@ -130,7 +142,7 @@ class SshRsyncWriteExtension(morphlib.writeexts.WriteExtension):
cliapp.ssh_runcmd(location, ['btrfs', 'subvolume', 'snapshot',
old_orig, new_orig])
- cliapp.runcmd(['rsync', '-a', '--checksum', '--numeric-ids',
+ cliapp.runcmd(['rsync', '-as', '--checksum', '--numeric-ids',
'--delete', temp_root, '%s:%s' % (location, new_orig)])
def get_old_orig(self, location, remote_mnt):
@@ -158,7 +170,7 @@ class SshRsyncWriteExtension(morphlib.writeexts.WriteExtension):
for name in image_names:
try_path = os.path.join(temp_root, 'boot', name)
if os.path.exists(try_path):
- cliapp.runcmd(['rsync', '-a', try_path,
+ cliapp.runcmd(['rsync', '-as', try_path,
'%s:%s' % (location, kernel_dest)])
def check_valid_target(self, location):
diff --git a/morphlib/exts/virtualbox-ssh.write b/morphlib/exts/virtualbox-ssh.write
index cb17b69b..3ee2eae0 100755
--- a/morphlib/exts/virtualbox-ssh.write
+++ b/morphlib/exts/virtualbox-ssh.write
@@ -101,24 +101,26 @@ class VirtualBoxPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
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,
+ cliapp.ssh_runcmd(ssh_host,
+ ['VBoxManage', 'convertfromraw', 'stdin', vdi_path,
str(os.path.getsize(raw_disk))],
stdin=f)
def create_virtualbox_guest(self, ssh_host, vm_name, vdi_path, autostart):
'''Create the VirtualBox virtual machine.'''
-
+
self.status(msg='Create VirtualBox virtual machine')
ram_mebibytes = str(self.get_ram_size() / (1024**2))
+ hostonly_iface = self.get_host_interface(ssh_host)
+
commands = [
['createvm', '--name', vm_name, '--ostype', 'Linux26_64',
'--register'],
['modifyvm', vm_name, '--ioapic', 'on', '--memory', ram_mebibytes,
- '--nic1', 'nat'],
+ '--nic1', 'hostonly', '--hostonlyadapter1', hostonly_iface,
+ '--nic2', 'nat', '--natnet2', 'default'],
['storagectl', vm_name, '--name', '"SATA Controller"',
'--add', 'sata', '--bootable', 'on', '--sataportcount', '2'],
['storageattach', vm_name, '--storagectl', '"SATA Controller"',
@@ -140,9 +142,65 @@ class VirtualBoxPlusSshWriteExtension(morphlib.writeexts.WriteExtension):
commands.append(['startvm', vm_name])
for command in commands:
- argv = ['ssh', ssh_host, 'VBoxManage'] + command
- cliapp.runcmd(argv)
+ argv = ['VBoxManage'] + command
+ cliapp.ssh_runcmd(ssh_host, argv)
+
+ def get_host_interface(self, ssh_host):
+ host_ipaddr = os.environ.get('HOST_IPADDR')
+ netmask = os.environ.get('NETMASK')
+ network_config = os.environ.get("NETWORK_CONFIG")
+ if network_config is None:
+ raise cliapp.AppException('NETWORK_CONFIG was not given')
+
+ if "eth0:" not in network_config:
+ raise cliapp.AppException(
+ 'NETWORK_CONFIG does not contain '
+ 'the eth0 configuration')
+
+ if "eth1:" not in network_config:
+ raise cliapp.AppException(
+ 'NETWORK_CONFIG does not contain '
+ 'the eth1 configuration')
+
+ if host_ipaddr is None:
+ raise cliapp.AppException('HOST_IPADDR was not given')
+
+ if netmask is None:
+ raise cliapp.AppException('NETMASK was not given')
+
+ # 'VBoxManage list hostonlyifs' retrieves a list with the hostonly
+ # interfaces on the host. For each interface, the following lines
+ # are shown on top:
+ #
+ # Name: vboxnet0
+ # GUID: 786f6276-656e-4074-8000-0a0027000000
+ # Dhcp: Disabled
+ # IPAddress: 192.168.100.1
+ #
+ # The following command tries to retrieve the hostonly interface
+ # name (e.g. vboxnet0) associated with the given ip address.
+ iface = None
+ lines = cliapp.ssh_runcmd(ssh_host,
+ ['VBoxManage', 'list', 'hostonlyifs']).splitlines()
+ for i, v in enumerate(lines):
+ if host_ipaddr in v:
+ iface = lines[i-3].split()[1]
+ break
+
+ if iface is None:
+ iface = cliapp.ssh_runcmd(ssh_host,
+ ['VBoxManage', 'hostonlyif', 'create'])
+ # 'VBoxManage hostonlyif create' shows the name of the
+ # created hostonly interface inside single quotes
+ iface = iface[iface.find("'") + 1 : iface.rfind("'")]
+ cliapp.ssh_runcmd(ssh_host,
+ ['VBoxManage', 'hostonlyif',
+ 'ipconfig', iface,
+ '--ip', host_ipaddr,
+ '--netmask', netmask])
+
+ return iface
VirtualBoxPlusSshWriteExtension().run()
diff --git a/morphlib/localartifactcache.py b/morphlib/localartifactcache.py
index b845cebf..76d085d1 100644
--- a/morphlib/localartifactcache.py
+++ b/morphlib/localartifactcache.py
@@ -14,6 +14,7 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+import collections
import os
import morphlib
@@ -115,3 +116,30 @@ class LocalArtifactCache(object):
for basename in basenames:
os.remove(os.path.join(dirname, basename))
+ def list_contents(self):
+ '''Return the set of sources cached and related information.
+
+ returns a [(cache_key, set(artifacts), last_used)]
+
+ '''
+ CacheInfo = collections.namedtuple('CacheInfo', ('artifacts', 'mtime'))
+ contents = collections.defaultdict(lambda: CacheInfo(set(), 0))
+ for dirpath, dirnames, filenames in os.walk(self.cachedir):
+ for filename in filenames:
+ cachekey = filename[:63]
+ artifact = filename[65:]
+ artifacts, max_mtime = contents[cachekey]
+ artifacts.add(artifact)
+ this_mtime = os.stat(os.path.join(dirpath, filename)).st_mtime
+ contents[cachekey] = CacheInfo(artifacts,
+ max(max_mtime, this_mtime))
+ return ((cache_key, info.artifacts, info.mtime)
+ for cache_key, info in contents.iteritems())
+
+
+ def remove(self, cachekey):
+ '''Remove all artifacts associated with the given cachekey.'''
+ for dirpath, dirnames, filenames in os.walk(self.cachedir):
+ for filename in filenames:
+ if filename.startswith(cachekey):
+ os.remove(os.path.join(dirpath, filename))
diff --git a/morphlib/localartifactcache_tests.py b/morphlib/localartifactcache_tests.py
index 36b5e891..082b926a 100644
--- a/morphlib/localartifactcache_tests.py
+++ b/morphlib/localartifactcache_tests.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 Codethink Limited
+# 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
@@ -47,8 +47,10 @@ class LocalArtifactCacheTests(unittest.TestCase):
'repo', 'ref', 'sha1', 'tree', morph, 'chunk.morph')
self.runtime_artifact = morphlib.artifact.Artifact(
self.source, 'chunk-runtime')
+ self.runtime_artifact.cache_key = '0'*64
self.devel_artifact = morphlib.artifact.Artifact(
self.source, 'chunk-devel')
+ self.devel_artifact.cache_key = '0'*64
def tearDown(self):
self.tempdir.remove()
@@ -155,3 +157,35 @@ class LocalArtifactCacheTests(unittest.TestCase):
cache.clear()
self.assertFalse(cache.has(self.runtime_artifact))
+ def test_put_artifacts_and_list_them_afterwards(self):
+ cache = morphlib.localartifactcache.LocalArtifactCache(
+ self.tempdir.dirname)
+
+ handle = cache.put(self.runtime_artifact)
+ handle.write('runtime')
+ handle.close()
+
+ self.assertTrue(len(list(cache.list_contents())) == 1)
+
+ handle = cache.put(self.devel_artifact)
+ handle.write('devel')
+ handle.close()
+
+ self.assertTrue(len(list(cache.list_contents())) == 1)
+
+ def test_put_artifacts_and_remove_them_afterwards(self):
+ cache = morphlib.localartifactcache.LocalArtifactCache(
+ self.tempdir.dirname)
+
+ handle = cache.put(self.runtime_artifact)
+ handle.write('runtime')
+ handle.close()
+
+ handle = cache.put(self.devel_artifact)
+ handle.write('devel')
+ handle.close()
+
+ key = list(cache.list_contents())[0][0]
+ cache.remove(key)
+
+ self.assertTrue(len(list(cache.list_contents())) == 0)
diff --git a/morphlib/plugins/gc_plugin.py b/morphlib/plugins/gc_plugin.py
new file mode 100644
index 00000000..ded4cf02
--- /dev/null
+++ b/morphlib/plugins/gc_plugin.py
@@ -0,0 +1,158 @@
+# 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 logging
+import os
+import shutil
+import time
+
+import cliapp
+
+import morphlib
+
+
+class GCPlugin(cliapp.Plugin):
+
+ def enable(self):
+ self.app.add_subcommand('gc', self.gc,
+ arg_synopsis='')
+ self.app.settings.integer(['cachedir-artifact-delete-older-than'],
+ 'always delete artifacts older than this '
+ 'period in seconds, (default: 1 week)',
+ metavar='PERIOD',
+ group="Storage Options",
+ default=(60*60*24*7))
+ self.app.settings.integer(['cachedir-artifact-keep-younger-than'],
+ 'allow deletion of artifacts older than '
+ 'this period in seconds, (default: 1 day)',
+ metavar='PERIOD',
+ group="Storage Options",
+ default=(60*60*24))
+
+ def disable(self):
+ pass
+
+ def gc(self, args):
+ '''Make space by removing unused files.
+
+ This removes all artifacts older than
+ --cachedir-artifact-delete-older-than, and may delete artifacts
+ older than --cachedir-artifact-keep-younger-than if it still
+ needs to make space.
+
+ This removes extracted chunks and staging areas for failed builds
+ from the directory specified by --tempdir.
+
+ '''
+
+ tempdir = self.app.settings['tempdir']
+ cachedir = self.app.settings['cachedir']
+ tempdir_min_space, cachedir_min_space = \
+ morphlib.util.unify_space_requirements(
+ tempdir, self.app.settings['tempdir-min-space'],
+ cachedir, self.app.settings['cachedir-min-space'])
+
+ self.cleanup_tempdir(tempdir, tempdir_min_space)
+ self.cleanup_cachedir(cachedir, cachedir_min_space)
+
+ def cleanup_tempdir(self, temp_path, min_space):
+ self.app.status(msg='Cleaning up temp dir %(temp_path)s',
+ temp_path=temp_path, chatty=True)
+ for subdir in ('failed', 'chunks'):
+ if morphlib.util.get_bytes_free_in_path(temp_path) >= min_space:
+ self.app.status(msg='Not Removing subdirectory '
+ '%(subdir)s, enough space already cleared',
+ subdir=os.path.join(temp_path, subdir),
+ chatty=True)
+ break
+ self.app.status(msg='Removing temp subdirectory: %(subdir)s',
+ subdir=subdir)
+ path = os.path.join(temp_path, subdir)
+ if os.path.exists(path):
+ shutil.rmtree(path)
+
+ def calculate_delete_range(self):
+ now = time.time()
+ always_delete_age = \
+ now - self.app.settings['cachedir-artifact-delete-older-than']
+ may_delete_age = \
+ now - self.app.settings['cachedir-artifact-keep-younger-than']
+ return always_delete_age, may_delete_age
+
+ def find_deletable_artifacts(self, lac, max_age, min_age):
+ '''Get a list of cache keys in order of how old they are.'''
+ contents = list(lac.list_contents())
+ always = set(cachekey
+ for cachekey, artifacts, mtime in contents
+ if mtime < max_age)
+ maybe = ((cachekey, mtime)
+ for cachekey, artifacts, mtime in contents
+ if max_age <= mtime < min_age)
+ return always, [cachekey for cachekey, mtime
+ in sorted(maybe, key=lambda x: x[1])]
+
+ def cleanup_cachedir(self, cache_path, min_space):
+ def sufficient_free():
+ free = morphlib.util.get_bytes_free_in_path(cache_path)
+ return (free >= min_space)
+ if sufficient_free():
+ self.app.status(msg='Not cleaning up cachedir, '
+ 'sufficient space already cleared',
+ chatty=True)
+ return
+ lac = morphlib.localartifactcache.LocalArtifactCache(cache_path)
+ max_age, min_age = self.calculate_delete_range()
+ logging.debug('Must remove artifacts older than timestamp %d'
+ % max_age)
+ always_delete, may_delete = \
+ self.find_deletable_artifacts(lac, max_age, min_age)
+ removed = 0
+ source_count = len(always_delete) + len(may_delete)
+ logging.debug('Must remove artifacts %s' % repr(always_delete))
+ logging.debug('Can remove artifacts %s' % repr(may_delete))
+
+ # Remove all old artifacts
+ for cachekey in always_delete:
+ self.app.status(msg='Removing source %(cachekey)s',
+ cachekey=cachekey, chatty=True)
+ lac.remove(cachekey)
+ removed += 1
+
+ # Maybe remove remaining middle-aged artifacts
+ for cachekey in may_delete:
+ if sufficient_free():
+ self.app.status(msg='Finished cleaning up cachedir with '
+ '%(remaining)d old sources remaining',
+ remaining=(source_count - removed),
+ chatty=True)
+ break
+ self.app.status(msg='Removing source %(cachekey)s',
+ cachekey=cachekey, chatty=True)
+ lac.remove(cachekey)
+ removed += 1
+
+ if sufficient_free():
+ self.app.status(msg='Made sufficient space in %(cache_path)s '
+ 'after removing %(removed)d sources',
+ removed=removed, cache_path=cache_path)
+ return
+ self.app.status(msg='Unable to clear enough space in %(cache_path)s '
+ 'after removing %(removed)d sources. Please '
+ 'reduce cachedir-artifact-keep-younger-than, '
+ 'clear space from elsewhere, enlarge the disk '
+ 'or reduce cachedir-min-space.',
+ cache_path=cache_path, removed=removed,
+ error=True)
diff --git a/morphlib/util.py b/morphlib/util.py
index 40420bf1..a9c22217 100644
--- a/morphlib/util.py
+++ b/morphlib/util.py
@@ -241,16 +241,29 @@ def on_same_filesystem(path_a, path_b): # pragma: no cover
# TODO: return true if one path is a subvolume of the other on btrfs?
return os.stat(path_a).st_dev == os.stat(path_b).st_dev
+def unify_space_requirements(tmp_path, tmp_min_size,
+ cache_path, cache_min_size): # pragma: no cover
+ """Adjust minimum sizes when paths share a disk.
+
+ Given pairs of path and minimum size, return the minimum sizes such
+ that when the paths are on the same disk, the sizes are added together.
+
+ """
+ # TODO: make this work for variable number of (path, size) pairs as needed
+ # hint: try list.sort and itertools.groupby
+ if not on_same_filesystem(tmp_path, cache_path):
+ return tmp_min_size, cache_min_size
+ unified_size = tmp_min_size + cache_min_size
+ return unified_size, unified_size
+
def check_disk_available(tmp_path, tmp_min_size,
cache_path, cache_min_size): # pragma: no cover
# if both are on the same filesystem, assume they share a storage pool,
# so the sum of the two sizes needs to be available
- # TODO: if we need to do this on any more than 2 filesystems
- # extend it to take a [(path, min)] and do some arcane mathematics
- # to split it into groups that share a filesystem
- # hint: try list.sort and itertools.groupby
- if on_same_filesystem(tmp_path, cache_path):
- tmp_min_size = cache_min_size = tmp_min_size + cache_min_size
+ # TODO: if we need to do this on any more than 2 paths
+ # extend it to take a [(path, min)]
+ tmp_min_size, cache_min_size = unify_space_requirements(
+ tmp_path, tmp_min_size, cache_path, cache_min_size)
tmp_size, cache_size = map(get_bytes_free_in_path, (tmp_path, cache_path))
errors = []
for path, min in [(tmp_path, tmp_min_size), (cache_path, cache_min_size)]:
@@ -260,4 +273,10 @@ def check_disk_available(tmp_path, tmp_min_size,
'has %(free)d' % locals())
if not errors:
return
- raise morphlib.Error('Insufficient space on disk:\n' + '\n'.join(errors))
+ raise morphlib.Error('Insufficient space on disk:\n' +
+ '\n'.join(errors) + '\n'
+ 'Please run `morph gc`. If the problem persists '
+ 'increase the disk size, manually clean up some '
+ 'space or reduce the disk space required by the '
+ 'tempdir-min-space and cachedir-min-space '
+ 'configuration options.')
diff --git a/morphlib/writeexts.py b/morphlib/writeexts.py
index df4cec33..de4189f8 100644
--- a/morphlib/writeexts.py
+++ b/morphlib/writeexts.py
@@ -71,9 +71,10 @@ class WriteExtension(cliapp.Application):
self.create_orig(version_root, temp_root)
self.create_fstab(version_root)
self.create_run(version_root)
+ os.symlink(version_label, os.path.join(mp, 'systems', 'default'))
if self.bootloader_is_wanted():
self.install_kernel(version_root, temp_root)
- self.install_extlinux(mp, version_label)
+ self.install_extlinux(mp)
except BaseException, e:
sys.stderr.write('Error creating disk image')
self.unmount(mp)
@@ -226,7 +227,7 @@ class WriteExtension(cliapp.Application):
cliapp.runcmd(['cp', '-a', try_path, kernel_dest])
break
- def install_extlinux(self, real_root, version_label):
+ def install_extlinux(self, real_root):
'''Install extlinux on the newly created disk image.'''
self.status(msg='Creating extlinux.conf')
@@ -235,9 +236,9 @@ class WriteExtension(cliapp.Application):
f.write('default linux\n')
f.write('timeout 1\n')
f.write('label linux\n')
- f.write('kernel /systems/' + version_label + '/kernel\n')
+ f.write('kernel /systems/default/kernel\n')
f.write('append root=/dev/sda '
- 'rootflags=subvol=systems/' + version_label + '/run '
+ 'rootflags=subvol=systems/default/run '
'init=/sbin/init rw\n')
self.status(msg='Installing extlinux')
diff --git a/without-test-modules b/without-test-modules
index 89b0bd8c..f143eb46 100644
--- a/without-test-modules
+++ b/without-test-modules
@@ -26,3 +26,4 @@ morphlib/plugins/__init__.py
morphlib/writeexts.py
morphlib/plugins/copy-artifacts_plugin.py
morphlib/plugins/trovectl_plugin.py
+morphlib/plugins/gc_plugin.py