summaryrefslogtreecommitdiff
path: root/morphlib
diff options
context:
space:
mode:
authorAdam Coldrick <adam.coldrick@codethink.co.uk>2015-02-03 17:40:29 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2015-04-22 10:06:51 +0000
commite258076f555ef5e66aa5888cbfc23bb40e50e72b (patch)
treee9ccd0f282993c86a69627214e33e87dc898b7f5 /morphlib
parentaa6dfcbb70c03dfeb3f9af02283aa1ab83667162 (diff)
downloadmorph-baserock/richardmaw/ostree-squash.tar.gz
Use OSTree for hardlink and artifact cache and a CoW unionfs to make system artifacts fasterbaserock/richardmaw/ostree-squash
This replaces the artifact cache and the hardlink cache with an OSTree repository, which is a great performance improvement when the cache directory and temporary directory are on the same filesystem. Additionally it can de-duplicate file contents. When we construct system artifacts deploy them, the staging area needs to be writable, so OSTree on its own is insufficient, as its hardlinks require the root to be kept read-only. To handle this we use either the in-kernel overlayfs or unionfs-fuse, though there is no automatic fall-back and it needs to be specified manually. To support distributed building, the artifact cache is extended to support an OSTree repository. Unfortunately cross-bootstrap is not expected to work with these changes at this point in time. IMPORTANT NOTE: We are well aware that this patch is too large to be comprehensible. We intend to revert and apply a cleaned up series when it is ready. Change-Id: I693bb752500dab3c6db3b97393689239ae7071a8
Diffstat (limited to 'morphlib')
-rw-r--r--morphlib/__init__.py2
-rw-r--r--morphlib/app.py30
-rw-r--r--morphlib/bins.py59
-rw-r--r--morphlib/bins_tests.py98
-rw-r--r--morphlib/buildcommand.py19
-rw-r--r--morphlib/builder.py131
-rw-r--r--morphlib/builder_tests.py18
-rw-r--r--morphlib/fsutils.py24
-rw-r--r--morphlib/localartifactcache.py16
-rw-r--r--morphlib/ostree.py178
-rw-r--r--morphlib/ostreeartifactcache.py301
-rw-r--r--morphlib/plugins/build_plugin.py5
-rw-r--r--morphlib/plugins/deploy_plugin.py120
-rw-r--r--morphlib/plugins/distbuild_plugin.py18
-rw-r--r--morphlib/plugins/gc_plugin.py7
-rw-r--r--morphlib/plugins/ostree_artifacts_plugin.py169
-rw-r--r--morphlib/remoteartifactcache.py25
-rw-r--r--morphlib/stagingarea.py87
-rw-r--r--morphlib/stagingarea_tests.py58
-rw-r--r--morphlib/util.py64
20 files changed, 1082 insertions, 347 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
index 0c9284d8..79e829a4 100644
--- a/morphlib/__init__.py
+++ b/morphlib/__init__.py
@@ -72,6 +72,8 @@ import morphologyfinder
import morphology
import morphloader
import morphset
+import ostree
+import ostreeartifactcache
import remoteartifactcache
import remoterepocache
import repoaliasresolver
diff --git a/morphlib/app.py b/morphlib/app.py
index 293b8517..2ec98dab 100644
--- a/morphlib/app.py
+++ b/morphlib/app.py
@@ -125,6 +125,22 @@ class Morph(cliapp.Application):
metavar='URL',
default=None,
group=group_advanced)
+ self.settings.string(['union-filesystem'],
+ 'filesystem used to provide "union filesystem" '
+ 'functionality when building and deploying. '
+ 'Only "overlayfs" and "unionfs-fuse" are '
+ 'supported at this time.',
+ default='overlayfs',
+ group=group_advanced)
+ self.settings.string(['ostree-repo-mode'],
+ 'Mode for OSTree artifact cache repository. If '
+ 'things will need to pull from your cache, this '
+ 'needs to be "archive_z2". Otherwise use '
+ '"bare". Note that archive_z2 will cause things '
+ 'involving the artifact cache (building and/or '
+ 'deploying) to be slow.',
+ default='bare',
+ group=group_advanced)
group_build = 'Build Options'
self.settings.integer(['max-jobs'],
@@ -159,12 +175,19 @@ class Morph(cliapp.Application):
'or /tmp because those are used internally '
'by things that cannot be on NFS, but '
'this setting can point at a directory in '
- 'NFS)',
+ 'NFS). If cachedir and tempdir are on separate '
+ 'filesystems, you will experience poor '
+ 'performance when building and deploying '
+ 'systems.',
metavar='DIR',
default=None,
group=group_storage)
self.settings.string(['cachedir'],
- 'cache git repositories and build results in DIR',
+ 'cache git repositories and build results in DIR.'
+ 'If cachedir and tempdir are on separate '
+ 'filesystems, you will experience poor '
+ 'performance when building and deploying '
+ 'systems.',
metavar='DIR',
group=group_storage,
default=defaults['cachedir'])
@@ -273,8 +296,7 @@ class Morph(cliapp.Application):
sys.exit(0)
tmpdir = self.settings['tempdir']
- for required_dir in (os.path.join(tmpdir, 'chunks'),
- os.path.join(tmpdir, 'staging'),
+ for required_dir in (os.path.join(tmpdir, 'staging'),
os.path.join(tmpdir, 'failed'),
os.path.join(tmpdir, 'deployments'),
self.settings['cachedir']):
diff --git a/morphlib/bins.py b/morphlib/bins.py
index 2e8ba0b3..c5bacc26 100644
--- a/morphlib/bins.py
+++ b/morphlib/bins.py
@@ -78,12 +78,8 @@ if sys.version_info < (2, 7, 3): # pragma: no cover
raise ExtractError("could not change owner")
tarfile.TarFile.chown = fixed_chown
-def create_chunk(rootdir, f, include, dump_memory_profile=None):
- '''Create a chunk from the contents of a directory.
-
- ``f`` is an open file handle, to which the tar file is written.
-
- '''
+def create_chunk(rootdir, chunkdir, include, dump_memory_profile=None):
+ '''Create a chunk from the contents of a directory.'''
dump_memory_profile = dump_memory_profile or (lambda msg: None)
@@ -91,31 +87,42 @@ def create_chunk(rootdir, f, include, dump_memory_profile=None):
# chunk artifact. This is useful to avoid problems from smallish
# clock skew. It needs to be recent enough, however, that GNU tar
# does not complain about an implausibly old timestamp.
- normalized_timestamp = 683074800
+ normalized_timestamp = (683074800, 683074800)
dump_memory_profile('at beginning of create_chunk')
-
- path_pairs = [(relname, os.path.join(rootdir, relname))
- for relname in include]
- tar = tarfile.open(fileobj=f, mode='w')
- for relname, filename in path_pairs:
+
+ def check_parent(name, paths):
+ parent = os.path.dirname(name)
+ if parent:
+ path = os.path.join(rootdir, parent)
+ if parent != rootdir and path not in paths:
+ paths.append(path)
+ check_parent(parent, paths)
+
+ def filter_contents(dirname, filenames):
+ paths = [os.path.join(rootdir, relname) for relname in include]
+ for name in include:
+ check_parent(name, paths)
+
+ return [f for f in filenames if os.path.join(dirname, f) not in paths]
+
+ logging.debug('Copying artifact into %s.' % chunkdir)
+ shutil.copytree(rootdir, chunkdir,
+ symlinks=True, ignore=filter_contents)
+
+ path_triplets = [(relname, os.path.join(chunkdir, relname),
+ os.path.join(rootdir, relname))
+ for relname in include]
+ for relname, filename, orig in path_triplets:
# Normalize mtime for everything.
- tarinfo = tar.gettarinfo(filename,
- arcname=relname)
- tarinfo.ctime = normalized_timestamp
- tarinfo.mtime = normalized_timestamp
- if tarinfo.isreg():
- with open(filename, 'rb') as f:
- tar.addfile(tarinfo, fileobj=f)
- else:
- tar.addfile(tarinfo)
- tar.close()
+ if not os.path.islink(filename):
+ os.utime(filename, normalized_timestamp)
- for relname, filename in reversed(path_pairs):
- if os.path.isdir(filename) and not os.path.islink(filename):
+ for relname, filename, orig in reversed(path_triplets):
+ if os.path.isdir(orig) and not os.path.islink(orig):
continue
else:
- os.remove(filename)
+ os.remove(orig)
dump_memory_profile('after removing in create_chunks')
@@ -209,7 +216,7 @@ def unpack_binary_from_file(f, dirname): # pragma: no cover
tf.close()
-def unpack_binary(filename, dirname):
+def unpack_binary(filename, dirname): # pragma: no cover
with open(filename, "rb") as f:
unpack_binary_from_file(f, dirname)
diff --git a/morphlib/bins_tests.py b/morphlib/bins_tests.py
index 3895680f..879aada4 100644
--- a/morphlib/bins_tests.py
+++ b/morphlib/bins_tests.py
@@ -78,11 +78,9 @@ class ChunkTests(BinsTest):
self.tempdir = tempfile.mkdtemp()
self.instdir = os.path.join(self.tempdir, 'inst')
self.chunk_file = os.path.join(self.tempdir, 'chunk')
- self.chunk_f = open(self.chunk_file, 'wb')
self.unpacked = os.path.join(self.tempdir, 'unpacked')
def tearDown(self):
- self.chunk_f.close()
shutil.rmtree(self.tempdir)
def populate_instdir(self):
@@ -108,109 +106,21 @@ class ChunkTests(BinsTest):
def create_chunk(self, includes):
self.populate_instdir()
- morphlib.bins.create_chunk(self.instdir, self.chunk_f, includes)
- self.chunk_f.flush()
-
- def unpack_chunk(self):
- os.mkdir(self.unpacked)
- morphlib.bins.unpack_binary(self.chunk_file, self.unpacked)
+ morphlib.bins.create_chunk(self.instdir, self.chunk_file, includes)
def test_empties_files(self):
self.create_chunk(['bin/foo', 'lib/libfoo.so'])
self.assertEqual([x for x, y in self.recursive_lstat(self.instdir)],
['.', 'bin', 'lib'])
- def test_creates_and_unpacks_chunk_exactly(self):
+ def test_creates_chunk_exactly(self):
self.create_chunk(['bin', 'bin/foo', 'lib', 'lib/libfoo.so'])
- self.unpack_chunk()
self.assertEqual(self.instdir_orig_files,
- self.recursive_lstat(self.unpacked))
+ self.recursive_lstat(self.chunk_file))
def test_uses_only_matching_names(self):
self.create_chunk(['bin/foo'])
- self.unpack_chunk()
- self.assertEqual([x for x, y in self.recursive_lstat(self.unpacked)],
+ self.assertEqual([x for x, y in self.recursive_lstat(self.chunk_file)],
['.', 'bin', 'bin/foo'])
self.assertEqual([x for x, y in self.recursive_lstat(self.instdir)],
['.', 'bin', 'lib', 'lib/libfoo.so'])
-
- def test_does_not_compress_artifact(self):
- self.create_chunk(['bin'])
- f = gzip.open(self.chunk_file)
- self.assertRaises(IOError, f.read)
- f.close()
-
-
-class ExtractTests(unittest.TestCase):
-
- def setUp(self):
- self.tempdir = tempfile.mkdtemp()
- self.instdir = os.path.join(self.tempdir, 'inst')
- self.unpacked = os.path.join(self.tempdir, 'unpacked')
-
- def tearDown(self):
- shutil.rmtree(self.tempdir)
-
- def create_chunk(self, callback):
- fh = StringIO.StringIO()
- os.mkdir(self.instdir)
- patterns = callback(self.instdir)
- morphlib.bins.create_chunk(self.instdir, fh, patterns)
- shutil.rmtree(self.instdir)
- fh.flush()
- fh.seek(0)
- return fh
-
- def test_extracted_files_replace_links(self):
- def make_linkfile(basedir):
- with open(os.path.join(basedir, 'babar'), 'w') as f:
- pass
- os.symlink('babar', os.path.join(basedir, 'bar'))
- return ['babar']
- linktar = self.create_chunk(make_linkfile)
-
- def make_file(basedir):
- with open(os.path.join(basedir, 'bar'), 'w') as f:
- pass
- return ['bar']
- filetar = self.create_chunk(make_file)
-
- os.mkdir(self.unpacked)
- morphlib.bins.unpack_binary_from_file(linktar, self.unpacked)
- morphlib.bins.unpack_binary_from_file(filetar, self.unpacked)
- mode = os.lstat(os.path.join(self.unpacked, 'bar')).st_mode
- self.assertTrue(stat.S_ISREG(mode))
-
- def test_extracted_dirs_keep_links(self):
- def make_usrlink(basedir):
- os.symlink('.', os.path.join(basedir, 'usr'))
- return ['usr']
- linktar = self.create_chunk(make_usrlink)
-
- def make_usrdir(basedir):
- os.mkdir(os.path.join(basedir, 'usr'))
- return ['usr']
- dirtar = self.create_chunk(make_usrdir)
-
- morphlib.bins.unpack_binary_from_file(linktar, self.unpacked)
- morphlib.bins.unpack_binary_from_file(dirtar, self.unpacked)
- mode = os.lstat(os.path.join(self.unpacked, 'usr')).st_mode
- self.assertTrue(stat.S_ISLNK(mode))
-
- def test_extracted_files_follow_links(self):
- def make_usrlink(basedir):
- os.symlink('.', os.path.join(basedir, 'usr'))
- return ['usr']
- linktar = self.create_chunk(make_usrlink)
-
- def make_usrdir(basedir):
- os.mkdir(os.path.join(basedir, 'usr'))
- with open(os.path.join(basedir, 'usr', 'foo'), 'w') as f:
- pass
- return ['usr', 'usr/foo']
- dirtar = self.create_chunk(make_usrdir)
-
- morphlib.bins.unpack_binary_from_file(linktar, self.unpacked)
- morphlib.bins.unpack_binary_from_file(dirtar, self.unpacked)
- mode = os.lstat(os.path.join(self.unpacked, 'foo')).st_mode
- self.assertTrue(stat.S_ISREG(mode))
diff --git a/morphlib/buildcommand.py b/morphlib/buildcommand.py
index fd5acdf5..a4670f0a 100644
--- a/morphlib/buildcommand.py
+++ b/morphlib/buildcommand.py
@@ -74,7 +74,8 @@ class BuildCommand(object):
This includes creating the directories on disk if they are missing.
'''
- return morphlib.util.new_artifact_caches(self.app.settings)
+ return morphlib.util.new_artifact_caches(
+ self.app.settings, status_cb=self.app.status)
def new_repo_caches(self):
return morphlib.util.new_repo_caches(self.app)
@@ -310,9 +311,8 @@ class BuildCommand(object):
self.build_source(source, build_env)
for a in artifacts:
- self.app.status(msg='%(kind)s %(name)s is cached at %(cachepath)s',
+ self.app.status(msg='%(kind)s %(name)s is cached.',
kind=source.morphology['kind'], name=a.name,
- cachepath=self.lac.artifact_filename(a),
chatty=(source.morphology['kind'] != "system"))
def build_source(self, source, build_env):
@@ -422,8 +422,10 @@ class BuildCommand(object):
# module into morphlib.remoteartififactcache first.
to_fetch = []
if not self.lac.has(artifact):
- to_fetch.append((self.rac.get(artifact),
- self.lac.put(artifact)))
+ self.app.status(
+ msg='Fetching to local cache: artifact %(name)s',
+ name=artifact.name)
+ self.lac.copy_from_remote(artifact, self.rac)
if artifact.source.morphology.needs_artifact_metadata_cached:
if not self.lac.has_artifact_metadata(artifact, 'meta'):
@@ -432,9 +434,6 @@ class BuildCommand(object):
self.lac.put_artifact_metadata(artifact, 'meta')))
if len(to_fetch) > 0:
- self.app.status(
- msg='Fetching to local cache: artifact %(name)s',
- name=artifact.name)
fetch_files(to_fetch)
def create_staging_area(self, build_env, use_chroot=True, extra_env={},
@@ -492,13 +491,13 @@ class BuildCommand(object):
if artifact.source.build_mode == 'bootstrap':
if not self.in_same_stratum(artifact.source, target_source):
continue
+
self.app.status(
msg='Installing chunk %(chunk_name)s from cache %(cache)s',
chunk_name=artifact.name,
cache=artifact.source.cache_key[:7],
chatty=True)
- handle = self.lac.get(artifact)
- staging_area.install_artifact(handle)
+ staging_area.install_artifact(self.lac, artifact)
if target_source.build_mode == 'staging':
morphlib.builder.ldconfig(self.app.runcmd, staging_area.dirname)
diff --git a/morphlib/builder.py b/morphlib/builder.py
index 1c016674..426c0ed0 100644
--- a/morphlib/builder.py
+++ b/morphlib/builder.py
@@ -28,7 +28,6 @@ import tempfile
import cliapp
import morphlib
-from morphlib.artifactcachereference import ArtifactCacheReference
from morphlib.util import error_message_for_containerised_commandline
import morphlib.gitversion
@@ -125,11 +124,7 @@ def ldconfig(runcmd, rootdir): # pragma: no cover
def download_depends(constituents, lac, rac, metadatas=None):
for constituent in constituents:
if not lac.has(constituent):
- source = rac.get(constituent)
- target = lac.put(constituent)
- shutil.copyfileobj(source, target)
- target.close()
- source.close()
+ lac.copy_from_remote(constituent, rac)
if metadatas is not None:
for metadata in metadatas:
if not lac.has_artifact_metadata(constituent, metadata):
@@ -246,28 +241,6 @@ class ChunkBuilder(BuilderBase):
'''Build chunk artifacts.'''
- def create_devices(self, destdir): # pragma: no cover
- '''Creates device nodes if the morphology specifies them'''
- morphology = self.source.morphology
- perms_mask = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
- if 'devices' in morphology and morphology['devices'] is not None:
- for dev in morphology['devices']:
- destfile = os.path.join(destdir, './' + dev['filename'])
- mode = int(dev['permissions'], 8) & perms_mask
- if dev['type'] == 'c':
- mode = mode | stat.S_IFCHR
- elif dev['type'] == 'b':
- mode = mode | stat.S_IFBLK
- else:
- raise IOError('Cannot create device node %s,'
- 'unrecognized device type "%s"'
- % (destfile, dev['type']))
- self.app.status(msg="Creating device node %s"
- % destfile)
- os.mknod(destfile, mode,
- os.makedev(dev['major'], dev['minor']))
- os.chown(destfile, dev['uid'], dev['gid'])
-
def build_and_cache(self): # pragma: no cover
with self.build_watch('overall-build'):
@@ -286,7 +259,6 @@ class ChunkBuilder(BuilderBase):
try:
self.get_sources(builddir)
self.run_commands(builddir, destdir, temppath, stdout)
- self.create_devices(destdir)
os.rename(temppath, logpath)
except BaseException as e:
@@ -459,13 +431,23 @@ class ChunkBuilder(BuilderBase):
extra_files += ['baserock/%s.meta' % chunk_artifact_name]
parented_paths = parentify(file_paths + extra_files)
- with self.local_artifact_cache.put(chunk_artifact) as f:
- self.write_metadata(destdir, chunk_artifact_name,
- parented_paths)
+ self.write_metadata(destdir, chunk_artifact_name,
+ parented_paths)
- self.app.status(msg='Creating chunk artifact %(name)s',
- name=chunk_artifact_name)
- morphlib.bins.create_chunk(destdir, f, parented_paths)
+ self.app.status(msg='Creating chunk artifact %(name)s',
+ name=chunk_artifact_name)
+ # TODO: This is not concurrency safe, bins.create_chunk will
+ # fail if tempdir already exists (eg if another build
+ # has created it).
+ tempdir = os.path.join(self.app.settings['tempdir'],
+ chunk_artifact.basename())
+ try:
+ morphlib.bins.create_chunk(destdir, tempdir,
+ parented_paths)
+ self.local_artifact_cache.put(tempdir, chunk_artifact)
+ finally:
+ if os.path.isdir(tempdir):
+ shutil.rmtree(tempdir)
built_artifacts.append(chunk_artifact)
for dirname, subdirs, files in os.walk(destdir):
@@ -509,8 +491,13 @@ class StratumBuilder(BuilderBase):
[x.name for x in constituents])
with lac.put_artifact_metadata(a, 'meta') as f:
json.dump(meta, f, indent=4, sort_keys=True)
- with self.local_artifact_cache.put(a) as f:
+ # TODO: This is not concurrency safe, put_stratum_artifact
+ # deletes temp which could be in use by another
+ # build.
+ temp = os.path.join(self.app.settings['tempdir'], a.name)
+ with open(temp, 'w+') as f:
json.dump([c.basename() for c in constituents], f)
+ self.local_artifact_cache.put_non_ostree_artifact(a, temp)
self.save_build_times()
return self.source.artifacts.values()
@@ -532,64 +519,47 @@ class SystemBuilder(BuilderBase): # pragma: no cover
arch = self.source.morphology['arch']
for a_name, artifact in self.source.artifacts.iteritems():
- handle = self.local_artifact_cache.put(artifact)
-
try:
fs_root = self.staging_area.destdir(self.source)
- self.unpack_strata(fs_root)
- self.write_metadata(fs_root, a_name)
- self.run_system_integration_commands(fs_root)
- unslashy_root = fs_root[1:]
- def uproot_info(info):
- info.name = relpath(info.name, unslashy_root)
- if info.islnk():
- info.linkname = relpath(info.linkname,
- unslashy_root)
- return info
- tar = tarfile.open(fileobj=handle, mode="w", name=a_name)
- self.app.status(msg='Constructing tarball of rootfs',
- chatty=True)
- tar.add(fs_root, recursive=True, filter=uproot_info)
- tar.close()
+ upperdir = self.staging_area.overlay_upperdir(
+ self.source)
+ editable_root = self.staging_area.overlaydir(self.source)
+ workdir = os.path.join(self.staging_area.dirname,
+ 'overlayfs-workdir')
+ if not os.path.exists(workdir):
+ os.makedirs(workdir)
+ union_filesystem = self.app.settings['union-filesystem']
+ morphlib.fsutils.overlay_mount(self.app.runcmd,
+ 'overlay-%s' % a_name,
+ editable_root, fs_root,
+ upperdir, workdir,
+ union_filesystem)
+ try:
+ self.unpack_strata(fs_root)
+ self.write_metadata(editable_root, a_name)
+ self.run_system_integration_commands(editable_root)
+ self.local_artifact_cache.put(editable_root, artifact)
+ finally:
+ morphlib.fsutils.unmount(self.app.runcmd,
+ editable_root)
except BaseException as e:
logging.error(traceback.format_exc())
self.app.status(msg='Error while building system',
error=True)
- handle.abort()
raise
- else:
- handle.close()
self.save_build_times()
return self.source.artifacts.itervalues()
- def load_stratum(self, stratum_artifact):
- '''Load a stratum from the local artifact cache.
-
- Returns a list of ArtifactCacheReference instances for the chunks
- contained in the stratum.
-
- '''
- cache = self.local_artifact_cache
- with cache.get(stratum_artifact) as stratum_file:
- try:
- artifact_list = json.load(stratum_file,
- encoding='unicode-escape')
- except ValueError as e:
- raise cliapp.AppException(
- 'Corruption detected: %s while loading %s' %
- (e, cache.artifact_filename(stratum_artifact)))
- return [ArtifactCacheReference(a) for a in artifact_list]
-
def unpack_one_stratum(self, stratum_artifact, target):
'''Unpack a single stratum into a target directory'''
cache = self.local_artifact_cache
- for chunk in self.load_stratum(stratum_artifact):
- self.app.status(msg='Unpacking chunk %(basename)s',
+ chunks = morphlib.util.get_stratum_contents(cache, stratum_artifact)
+ for chunk in chunks:
+ self.app.status(msg='Checkout chunk %(basename)s',
basename=chunk.basename(), chatty=True)
- with cache.get(chunk) as chunk_file:
- morphlib.bins.unpack_binary_from_file(chunk_file, target)
+ cache.get(chunk, target)
target_metadata = os.path.join(
target, 'baserock', '%s.meta' % stratum_artifact.name)
@@ -600,7 +570,7 @@ class SystemBuilder(BuilderBase): # pragma: no cover
def unpack_strata(self, path):
'''Unpack strata into a directory.'''
- self.app.status(msg='Unpacking strata to %(path)s',
+ self.app.status(msg='Checking out strata to %(path)s',
path=path, chatty=True)
with self.build_watch('unpack-strata'):
for a_name, a in self.source.artifacts.iteritems():
@@ -612,7 +582,8 @@ class SystemBuilder(BuilderBase): # pragma: no cover
# download the chunk artifacts if necessary
for stratum_artifact in self.source.dependencies:
- chunks = self.load_stratum(stratum_artifact)
+ chunks = morphlib.util.get_stratum_contents(
+ self.local_artifact_cache, stratum_artifact)
download_depends(chunks,
self.local_artifact_cache,
self.remote_artifact_cache)
diff --git a/morphlib/builder_tests.py b/morphlib/builder_tests.py
index a571e3d0..b5e66521 100644
--- a/morphlib/builder_tests.py
+++ b/morphlib/builder_tests.py
@@ -105,8 +105,8 @@ class FakeArtifactCache(object):
def __init__(self):
self._cached = {}
- def put(self, artifact):
- return FakeFileHandle(self, (artifact.cache_key, artifact.name))
+ def put(self, artifact, directory):
+ self._cached[(artifact.cache_key, artifact.name)] = artifact.name
def put_artifact_metadata(self, artifact, name):
return FakeFileHandle(self, (artifact.cache_key, artifact.name, name))
@@ -114,7 +114,7 @@ class FakeArtifactCache(object):
def put_source_metadata(self, source, cachekey, name):
return FakeFileHandle(self, (cachekey, name))
- def get(self, artifact):
+ def get(self, artifact, directory=None):
return StringIO.StringIO(
self._cached[(artifact.cache_key, artifact.name)])
@@ -134,6 +134,10 @@ class FakeArtifactCache(object):
def has_source_metadata(self, source, cachekey, name):
return (cachekey, name) in self._cached
+ def copy_from_remote(self, artifact, remote):
+ self._cached[(artifact.cache_key, artifact.name)] = \
+ remote._cached[(artifact.cache_key, artifact.name)]
+
class BuilderBaseTests(unittest.TestCase):
@@ -191,9 +195,7 @@ class BuilderBaseTests(unittest.TestCase):
rac = FakeArtifactCache()
afacts = [FakeArtifact(name) for name in ('a', 'b', 'c')]
for a in afacts:
- fh = rac.put(a)
- fh.write(a.name)
- fh.close()
+ rac.put(a, 'not-a-dir')
morphlib.builder.download_depends(afacts, lac, rac)
self.assertTrue(all(lac.has(a) for a in afacts))
@@ -202,9 +204,7 @@ class BuilderBaseTests(unittest.TestCase):
rac = FakeArtifactCache()
afacts = [FakeArtifact(name) for name in ('a', 'b', 'c')]
for a in afacts:
- fh = rac.put(a)
- fh.write(a.name)
- fh.close()
+ rac.put(a, 'not-a-dir')
fh = rac.put_artifact_metadata(a, 'meta')
fh.write('metadata')
fh.close()
diff --git a/morphlib/fsutils.py b/morphlib/fsutils.py
index a3b73bf6..6e1adc90 100644
--- a/morphlib/fsutils.py
+++ b/morphlib/fsutils.py
@@ -46,14 +46,34 @@ def create_fs(runcmd, partition): # pragma: no cover
runcmd(['mkfs.btrfs', '-L', 'baserock', partition])
-def mount(runcmd, partition, mount_point, fstype=None): # pragma: no cover
+def mount(runcmd, partition, mount_point,
+ fstype=None, options=[]): # pragma: no cover
if not os.path.exists(mount_point):
os.mkdir(mount_point)
if not fstype:
fstype = []
else:
fstype = ['-t', fstype]
- runcmd(['mount', partition, mount_point] + fstype)
+ argv = ['mount', partition, mount_point] + fstype
+ for option in options:
+ argv.extend(('-o', option))
+ runcmd(argv)
+
+
+def overlay_mount(runcmd, partition, mount_point,
+ lowerdir, upperdir, workdir, method): # pragma: no cover
+ if method == 'overlayfs':
+ options = ['lowerdir=%s' % lowerdir, 'upperdir=%s' % upperdir,
+ 'workdir=%s' % workdir]
+ mount(runcmd, partition, mount_point, 'overlay', options)
+ elif method == 'unionfs-fuse':
+ if not os.path.exists(mount_point):
+ os.mkdir(mount_point)
+ dir_string = '%s=RW:%s=RO' % (upperdir, lowerdir)
+ runcmd(['unionfs', '-o', 'cow', '-o', 'hide_meta_files',
+ dir_string, mount_point])
+ else:
+ raise Exception('Union filesystem %s not supported' % method)
def unmount(runcmd, mount_point): # pragma: no cover
diff --git a/morphlib/localartifactcache.py b/morphlib/localartifactcache.py
index e6695c4e..1b834a32 100644
--- a/morphlib/localartifactcache.py
+++ b/morphlib/localartifactcache.py
@@ -129,11 +129,21 @@ class LocalArtifactCache(object):
returns a [(cache_key, set(artifacts), last_used)]
'''
+ def is_artifact(filename):
+ # This is just enough to avoid crashes from random unpacked
+ # directory trees and temporary files in the cachedir. A
+ # better mechanism is needed. It's not simple to tell
+ # OSFS.walkfiles() to do a non-recursive walk, sadly.
+ return '.' in filename and '/' not in filename
+
CacheInfo = collections.namedtuple('CacheInfo', ('artifacts', 'mtime'))
contents = collections.defaultdict(lambda: CacheInfo(set(), 0))
for filename in self.cachefs.walkfiles():
- cachekey = filename[:63]
- artifact = filename[65:]
+ # filenames are returned with a preceeding /.
+ filename = filename[1:]
+ if not is_artifact(filename): # pragma: no cover
+ continue
+ cachekey, artifact = filename.split('.', 1)
artifacts, max_mtime = contents[cachekey]
artifacts.add(artifact)
art_info = self.cachefs.getinfo(filename)
@@ -146,5 +156,5 @@ class LocalArtifactCache(object):
def remove(self, cachekey):
'''Remove all artifacts associated with the given cachekey.'''
for filename in (x for x in self.cachefs.walkfiles()
- if x.startswith(cachekey)):
+ if x[1:].startswith(cachekey)):
self.cachefs.remove(filename)
diff --git a/morphlib/ostree.py b/morphlib/ostree.py
new file mode 100644
index 00000000..ed2c59da
--- /dev/null
+++ b/morphlib/ostree.py
@@ -0,0 +1,178 @@
+# Copyright (C) 2013-2015 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, see <http://www.gnu.org/licenses/>.
+#
+# =*= License: GPL-2 =*=
+
+
+from gi.repository import OSTree
+from gi.repository import Gio
+from gi.repository import GLib
+
+import os
+import logging
+
+
+class OSTreeRepo(object):
+
+ """Class to wrap the OSTree API."""
+
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS = Gio.FileQueryInfoFlags(1)
+ OSTREE_GIO_FAST_QUERYINFO = (
+ 'standard::name,'
+ 'standard::type,'
+ 'standard::size,'
+ 'standard::is-symlink,'
+ 'standard::symlink-target,'
+ 'unix::device,'
+ 'unix::inode,'
+ 'unix::mode,'
+ 'unix::uid,'
+ 'unix::gid,'
+ 'unix::rdev')
+
+ def __init__(self, path, disable_fsync=True, mode='bare'):
+ self.path = path
+ self.repo = self._open_repo(path, disable_fsync, mode)
+
+ def _open_repo(self, path, disable_fsync=True, mode='bare'):
+ """Create and open and OSTree.Repo, and return it."""
+ repo_dir = Gio.file_new_for_path(path)
+ repo = OSTree.Repo.new(repo_dir)
+ logging.debug('using %s' % mode)
+ if mode == 'bare':
+ mode = OSTree.RepoMode.BARE
+ elif mode == 'archive_z2':
+ mode = OSTree.RepoMode.ARCHIVE_Z2
+ else:
+ raise Exception('Mode %s is not supported' % mode)
+
+ try:
+ repo.open(None)
+ logging.debug('opened')
+ except GLib.GError:
+ if not os.path.exists(path):
+ os.makedirs(path)
+ logging.debug('failed to open, creating')
+ repo.create(mode, None)
+ repo.set_disable_fsync(disable_fsync)
+ return repo
+
+ def refsdir(self):
+ """Return the abspath to the refs/heads directory in the repo."""
+ return os.path.join(os.path.abspath(self.path), 'refs/heads')
+
+ def touch_ref(self, ref):
+ """Update the mtime of a ref file in repo/refs/heads."""
+ os.utime(os.path.join(self.refsdir(), ref), None)
+
+ def resolve_rev(self, branch, allow_noent=True):
+ """Return the SHA256 corresponding to 'branch'."""
+ return self.repo.resolve_rev(branch, allow_noent)[1]
+
+ def read_commit(self, branch):
+ """Return an OSTree.RepoFile representing a committed tree."""
+ return self.repo.read_commit(branch, None)[1]
+
+ def query_info(self, file_object):
+ """Quickly return a Gio.FileInfo for file_object."""
+ return file_object.query_info(self.OSTREE_GIO_FAST_QUERYINFO,
+ self.G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ None)
+
+ def checkout(self, branch, destdir):
+ """Checkout branch into destdir."""
+ checkout_path = destdir
+ if not os.path.exists(checkout_path):
+ os.makedirs(checkout_path)
+ checkout = Gio.file_new_for_path(checkout_path)
+
+ commit = self.read_commit(branch)
+ commit_info = self.query_info(commit)
+ self.repo.checkout_tree(OSTree.RepoCheckoutMode.NONE,
+ OSTree.RepoCheckoutOverwriteMode.UNION_FILES,
+ checkout, commit,commit_info, None)
+
+ def commit(self, subject, srcdir, branch, body=''):
+ """Commit the contents of 'srcdir' to 'branch'.
+
+ The subject parameter is the title of the commit message, and the
+ body parameter is the body of the commit message.
+
+ """
+ self.repo.prepare_transaction(None)
+ parent = self.resolve_rev(branch)
+ mtree = OSTree.MutableTree()
+ src = Gio.file_new_for_path(srcdir)
+ self.repo.write_directory_to_mtree(src, mtree, None, None)
+ root = self.repo.write_mtree(mtree, None)[1]
+ checksum = self.repo.write_commit(parent, subject, body,
+ None, root, None)[1]
+ self.repo.transaction_set_ref(None, branch, checksum)
+ stats = self.repo.commit_transaction(None)
+
+ def cat_file(self, ref, path):
+ """Return the file descriptor of path at ref."""
+ commit = self.read_commit(ref)
+ relative = commit.resolve_relative_path(path)
+ ret, content, etag = relative.load_contents()
+ return content
+
+ def list_refs(self, ref=None, resolved=False):
+ """Return a list of all refs in the repo."""
+ if ref:
+ refs = self.repo.list_refs(ref)[1]
+ else:
+ refs = self.repo.list_refs()[1]
+ if not resolved:
+ return refs.keys()
+ return refs
+
+ def delete_ref(self, ref):
+ """Remove refspec from the repo."""
+ if not self.list_refs(ref):
+ raise Exception("Failed to delete ref, it doesn't exist")
+ self.repo.set_ref_immediate(None, ref, None, None)
+
+ def prune(self):
+ """Remove unreachable objects from the repo."""
+ depth = -1 # no recursion limit
+ return self.repo.prune(OSTree.RepoPruneFlags.REFS_ONLY, depth, None)
+
+ def add_remote(self, name, url):
+ """Add a remote with a given name and url."""
+ options_type = GLib.VariantType.new('a{sv}')
+ options_builder = GLib.VariantBuilder.new(options_type)
+ options = options_builder.end()
+ self.repo.remote_add(name, url, options, None)
+
+ def remove_remote(self, name):
+ """Remove a remote with a given name."""
+ self.repo.remote_delete(name, None)
+
+ def get_remote_url(self, name):
+ """Return the URL for a remote."""
+ return self.repo.remote_get_url(name)[1]
+
+ def list_remotes(self):
+ """Return a list of all remotes for this repo."""
+ return self.repo.remote_list()
+
+ def has_remote(self, name):
+ """Return True if name is a remote for the repo."""
+ return name in self.list_remotes()
+
+ def pull(self, refs, remote):
+ """Pull ref from remote into the local repo."""
+ flags = OSTree.RepoPullFlags.NONE
+ self.repo.pull(remote, refs, flags, None, None)
diff --git a/morphlib/ostreeartifactcache.py b/morphlib/ostreeartifactcache.py
new file mode 100644
index 00000000..8176f499
--- /dev/null
+++ b/morphlib/ostreeartifactcache.py
@@ -0,0 +1,301 @@
+# Copyright (C) 2015 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 collections
+import contextlib
+import logging
+import os
+import shutil
+import stat
+import string
+import tarfile
+import tempfile
+
+import cliapp
+from gi.repository import GLib
+
+import morphlib
+from morphlib.artifactcachereference import ArtifactCacheReference
+
+
+class NotCachedError(morphlib.Error):
+
+ def __init__(self, ref):
+ self.msg = 'Failed to checkout %s from the artifact cache.' % ref
+
+
+class OSTreeArtifactCache(object):
+ """Class to provide the artifact cache API using an OSTree repo."""
+
+ def __init__(self, cachedir, mode='bare', status_cb=None):
+ repo_dir = os.path.join(cachedir, 'repo')
+ self.repo = morphlib.ostree.OSTreeRepo(repo_dir, mode=mode)
+ self.cachedir = cachedir
+ self.status_cb = status_cb
+
+ def status(self, *args, **kwargs):
+ if self.status_cb is not None:
+ self.status_cb(*args, **kwargs)
+
+ @contextlib.contextmanager
+ def _get_file_from_remote(self, artifact, remote, metadata_name=None):
+ if metadata_name:
+ handle = remote.get_artifact_metadata(artifact, metadata_name)
+ self.status(
+ msg='Downloading %(name)s %(metadata_name)s as a file.',
+ chatty=True, name=artifact.basename(),
+ metadata_name=metadata_name)
+ else:
+ handle = remote.get(artifact)
+ self.status(
+ msg='Downloading %(name)s as a tarball.', chatty=True,
+ name=artifact.basename())
+
+ try:
+ temporary_download = tempfile.NamedTemporaryFile(dir=self.cachedir)
+ shutil.copyfileobj(handle, temporary_download)
+ temporary_download.flush()
+ yield temporary_download.name
+ finally:
+ temporary_download.close()
+
+ def _get_artifact_cache_name(self, artifact):
+ valid_chars = string.digits + string.letters + '-._'
+ transl = lambda x: x if x in valid_chars else '_'
+ key = ''.join([transl(x) for x in artifact.basename()])
+ return key
+
+ def put(self, directory, artifact):
+ """Commit the contents of 'directory' to the repo.
+
+ This uses the artifact name and cache key to create the ref, so the
+ contents of directory should be the contents of the artifact.
+
+ """
+ cache_key, kind, name = artifact.basename().split('.', 2)
+ ref = self._get_artifact_cache_name(artifact)
+ subject = name
+ try:
+ self.status(
+ msg='Committing %(subject)s to artifact cache at %(ref)s.',
+ chatty=True, subject=subject, ref=ref)
+ self.repo.commit(subject, directory, ref)
+ except GLib.GError as e:
+ logging.debug('OSTree raised an exception: %s' % e)
+ raise cliapp.AppException('Failed to commit %s to artifact '
+ 'cache.' % ref)
+
+ def put_non_ostree_artifact(self, artifact, location, metadata_name=None):
+ """Store a single file in the artifact cachedir."""
+ if metadata_name:
+ filename = self._artifact_metadata_filename(artifact,
+ metadata_name)
+ else:
+ filename = self.artifact_filename(artifact)
+ shutil.copy(location, filename)
+
+ def _remove_device_nodes(self, path):
+ for dirpath, dirnames, filenames in os.walk(path):
+ for f in filenames:
+ filepath = os.path.join(dirpath, f)
+ mode = os.lstat(filepath).st_mode
+ if stat.S_ISBLK(mode) or stat.S_ISCHR(mode):
+ logging.debug('Removing device node %s from artifact' %
+ filepath)
+ os.remove(filepath)
+
+ def _copy_metadata_from_remote(self, artifact, remote):
+ """Copy a metadata file from a remote cache."""
+ a, name = artifact.basename().split('.', 1)
+ with self._get_file_from_remote(ArtifactCacheReference(a),
+ remote, name) as location:
+ self.put_non_ostree_artifact(ArtifactCacheReference(a),
+ location, name)
+
+ def copy_from_remote(self, artifact, remote):
+ """Get 'artifact' from remote artifact cache and store it locally.
+
+ This takes an Artifact object and a RemoteArtifactCache. Note that
+ `remote` here is not the same as a `remote` for and OSTree repo.
+
+ """
+ if remote.method == 'tarball':
+ with self._get_file_from_remote(artifact, remote) as location:
+ try:
+ cache_key, kind, name = artifact.basename().split('.', 2)
+ except ValueError:
+ # We can't split the name properly, it must be metadata!
+ self._copy_metadata_from_remote(artifact, remote)
+ return
+
+ if kind == 'stratum':
+ self.put_non_ostree_artifact(artifact, location)
+ return
+ try:
+ tempdir = tempfile.mkdtemp(dir=self.cachedir)
+ with tarfile.open(name=location) as tf:
+ tf.extractall(path=tempdir)
+ self._remove_device_nodes(tempdir)
+ self.put(tempdir, artifact)
+ except tarfile.ReadError:
+ # Reading the tarball failed, and we expected a
+ # tarball artifact. Something must have gone
+ # wrong.
+ raise
+ finally:
+ shutil.rmtree(tempdir)
+
+ elif remote.method == 'ostree':
+ self.status(msg='Pulling artifact for %(name)s from remote.',
+ chatty=True, name=artifact.basename())
+ ref = self._get_artifact_cache_name(artifact)
+ try:
+ cache_key, kind, name = ref.split('.', 2)
+ except ValueError:
+ # if we can't split the name properly, we must want metadata
+ self._copy_metadata_from_remote(artifact, remote)
+ return
+
+ if artifact.basename().split('.', 2)[1] == 'stratum':
+ with self._get_file_from_remote(artifact, remote) as location:
+ self.put_non_ostree_artifact(artifact, location)
+ return
+
+ try:
+ if not self.repo.has_remote(remote.name):
+ self.repo.add_remote(remote.name, remote.ostree_url)
+ self.repo.pull([ref], remote.name)
+ except GLib.GError as e:
+ logging.debug('OSTree raised an exception: %s' % e)
+ raise cliapp.AppException('Failed to pull %s from remote '
+ 'cache.' % ref)
+
+ def get(self, artifact, directory=None):
+ """Checkout an artifact from the repo and return its location."""
+ cache_key, kind, name = artifact.basename().split('.', 2)
+ if kind == 'stratum':
+ return self.artifact_filename(artifact)
+ if directory is None:
+ directory = tempfile.mkdtemp()
+ ref = self._get_artifact_cache_name(artifact)
+ try:
+ self.repo.checkout(ref, directory)
+ # We need to update the mtime and atime of the ref file in the
+ # repository so that we can decide which refs were least recently
+ # accessed when doing `morph gc`.
+ self.repo.touch_ref(ref)
+ except GLib.GError as e:
+ logging.debug('OSTree raised an exception: %s' % e)
+ raise NotCachedError(ref)
+ return directory
+
+ 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 ref in self.repo.list_refs():
+ cachekey = ref[:63]
+ artifact = ref[65:]
+ artifacts, max_mtime = contents[cachekey]
+ artifacts.add(artifact)
+ ref_filename = os.path.join(self.repo.refsdir(), ref)
+ mtime = os.path.getmtime(ref_filename)
+ contents[cachekey] = CacheInfo(artifacts, max(max_mtime, 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 ref in (r for r in self.repo.list_refs()
+ if r.startswith(cachekey)):
+ self.repo.delete_ref(ref)
+
+ def prune(self):
+ """Delete orphaned objects in the repo."""
+ self.repo.prune()
+
+ def has(self, artifact):
+ try:
+ cachekey, kind, name = artifact.basename().split('.', 2)
+ except ValueError:
+ # We couldn't split the basename properly, we must want metadata
+ cachekey, name = artifact.basename().split('.', 1)
+ if self.has_artifact_metadata(artifact, name):
+ return True
+ else:
+ return False
+
+ if kind == 'stratum':
+ if self._has_file(self.artifact_filename(artifact)):
+ return True
+ else:
+ return False
+
+ sha = self.repo.resolve_rev(self._get_artifact_cache_name(artifact))
+ if sha:
+ # We call touch_ref here to help `morph gc` work out which
+ # artifacts have been used most and least recently.
+ self.repo.touch_ref(self._get_artifact_cache_name(artifact))
+ return True
+ return False
+
+ def get_artifact_metadata(self, artifact, name):
+ filename = self._artifact_metadata_filename(artifact, name)
+ os.utime(filename, None)
+ return open(filename)
+
+ def get_source_metadata_filename(self, source, cachekey, name):
+ return self._source_metadata_filename(source, cachekey, name)
+
+ def get_source_metadata(self, source, cachekey, name):
+ filename = self._source_metadata_filename(source, cachekey, name)
+ os.utime(filename, None)
+ return open(filename)
+
+ def artifact_filename(self, artifact):
+ return os.path.join(self.cachedir, artifact.basename())
+
+ def _artifact_metadata_filename(self, artifact, name):
+ return os.path.join(self.cachedir, artifact.metadata_basename(name))
+
+ def _source_metadata_filename(self, source, cachekey, name):
+ return os.path.join(self.cachedir, '%s.%s' % (cachekey, name))
+
+ def put_artifact_metadata(self, artifact, name):
+ filename = self._artifact_metadata_filename(artifact, name)
+ return morphlib.savefile.SaveFile(filename, mode='w')
+
+ def put_source_metadata(self, source, cachekey, name):
+ filename = self._source_metadata_filename(source, cachekey, name)
+ return morphlib.savefile.SaveFile(filename, mode='w')
+
+ def _has_file(self, filename):
+ if os.path.exists(filename):
+ os.utime(filename, None)
+ return True
+ return False
+
+ def has_artifact_metadata(self, artifact, name):
+ filename = self._artifact_metadata_filename(artifact, name)
+ return self._has_file(filename)
+
+ def has_source_metadata(self, source, cachekey, name):
+ filename = self._source_metadata_filename(source, cachekey, name)
+ return self._has_file(filename)
diff --git a/morphlib/plugins/build_plugin.py b/morphlib/plugins/build_plugin.py
index b8569ff7..168f83c9 100644
--- a/morphlib/plugins/build_plugin.py
+++ b/morphlib/plugins/build_plugin.py
@@ -327,7 +327,6 @@ class BuildPlugin(cliapp.Plugin):
for name, component in components.iteritems():
component.build_env = root.build_env
bc.build_in_order(component)
- self.app.status(msg='%(kind)s %(name)s is cached at %(path)s',
+ self.app.status(msg='%(kind)s %(name)s is cached.',
kind=component.source.morphology['kind'],
- name=name,
- path=bc.lac.artifact_filename(component))
+ name=name)
diff --git a/morphlib/plugins/deploy_plugin.py b/morphlib/plugins/deploy_plugin.py
index ea84d9ec..231fa868 100644
--- a/morphlib/plugins/deploy_plugin.py
+++ b/morphlib/plugins/deploy_plugin.py
@@ -25,6 +25,15 @@ import warnings
import cliapp
import morphlib
+from morphlib.artifactcachereference import ArtifactCacheReference
+
+
+class NotYetBuiltError(morphlib.Error):
+
+ def __init__(self, name):
+ self.msg = ('Deployment failed as %s is not yet built.\n'
+ 'Please ensure the system is built before deployment.'
+ % name)
def configuration_for_system(system_id, vars_from_commandline,
@@ -419,6 +428,8 @@ class DeployPlugin(cliapp.Plugin):
system_status_prefix = '%s[%s]' % (old_status_prefix, system['morph'])
self.app.status_prefix = system_status_prefix
try:
+ system_tree = None
+
# Find the artifact to build
morph = morphlib.util.sanitise_morphology_path(system['morph'])
srcpool = build_command.create_source_pool(build_repo, ref, morph)
@@ -467,6 +478,9 @@ class DeployPlugin(cliapp.Plugin):
system_tree, deploy_location)
finally:
self.app.status_prefix = system_status_prefix
+ if system_tree and os.path.exists(system_tree):
+ morphlib.fsutils.unmount(self.app.runcmd, system_tree)
+ shutil.rmtree(system_tree)
finally:
self.app.status_prefix = old_status_prefix
@@ -525,46 +539,106 @@ class DeployPlugin(cliapp.Plugin):
except morphlib.extensions.ExtensionNotFoundError:
pass
+ def checkout_system(self, build_command, artifact, path):
+ """Checkout a system into `path`.
+
+ This checks out the system artifact into the directory given by
+ `path`. If the system is not in the local cache, it is first fetched
+ from the remote cache.
+
+ Raises a NotYetBuiltError if the system artifact isn't cached either
+ locally or remotely.
+
+ """
+ try:
+ self.app.status(msg='Checking out system for configuration')
+ build_command.cache_artifacts_locally([artifact])
+ build_command.lac.get(artifact, path)
+ self.create_device_nodes(artifact, path)
+ except (morphlib.ostreeartifactcache.NotCachedError,
+ morphlib.remoteartifactcache.GetError):
+ raise NotYetBuiltError(artifact.name)
+
+ self.app.status(
+ msg='System checked out at %(system_tree)s',
+ system_tree=path)
+
+ def create_device_nodes(self, artifact, path):
+ self.fix_chunk_build_mode(artifact)
+ for a in artifact.walk():
+ morph = a.source.morphology
+ if morph['kind'] == 'chunk' and \
+ morph['build-mode'] != 'bootstrap':
+ morphlib.util.create_devices(a.source.morphology, path)
+
+ def fix_chunk_build_mode(self, system_artifact):
+ """Give each chunk's in-memory morpholgy the correct build-mode."""
+ strata = set(a for a in system_artifact.walk()
+ if a.source.morphology['kind'] == 'stratum')
+ chunks = set(a for a in system_artifact.walk()
+ if a.source.morphology['kind'] == 'chunk')
+ for chunk in chunks:
+ for stratum in strata:
+ for spec in stratum.source.morphology['chunks']:
+ if chunk.source.morphology['name'] == spec['name']:
+ chunk.source.morphology['build-mode'] = \
+ spec['build-mode']
+
def setup_deploy(self, build_command, deploy_tempdir, root_repo_dir, ref,
artifact, deployment_type, location, env):
+ """Checkout the artifact, create metadata and return the location.
+
+ This checks out the system into a temporary directory, and then mounts
+ this temporary directory alongside a different temporary directory
+ using a union filesystem. This allows changes to be made without
+ touching the checked out artifacts. The deployment metadata file is
+ created and then the directory at which the two temporary directories
+ are mounted is returned.
+
+ """
# deployment_type, location and env are only used for saving metadata
- # Create a tempdir to extract the rootfs in
- system_tree = tempfile.mkdtemp(dir=deploy_tempdir)
+ deployment_dir = tempfile.mkdtemp(dir=deploy_tempdir)
+ # Create a tempdir to extract the rootfs in
+ system_tree = tempfile.mkdtemp(dir=deployment_dir)
+
+ # Create temporary directory for overlayfs
+ overlay_dir = os.path.join(deployment_dir,
+ '%s-upperdir' % artifact.name)
+ if not os.path.exists(overlay_dir):
+ os.makedirs(overlay_dir)
+ work_dir = os.path.join(deployment_dir, '%s-workdir' % artifact.name)
+ if not os.path.exists(work_dir):
+ os.makedirs(work_dir)
+
+ deploy_tree = os.path.join(deployment_dir,
+ 'overlay-deploy-%s' % artifact.name)
try:
- # Unpack the artifact (tarball) to a temporary directory.
- self.app.status(msg='Unpacking system for configuration')
-
- if build_command.lac.has(artifact):
- f = build_command.lac.get(artifact)
- elif build_command.rac.has(artifact):
- build_command.cache_artifacts_locally([artifact])
- f = build_command.lac.get(artifact)
- else:
- raise cliapp.AppException('Deployment failed as system is'
- ' not yet built.\nPlease ensure'
- ' the system is built before'
- ' deployment.')
- tf = tarfile.open(fileobj=f)
- tf.extractall(path=system_tree)
+ self.checkout_system(build_command, artifact, system_tree)
- self.app.status(
- msg='System unpacked at %(system_tree)s',
- system_tree=system_tree)
+ union_filesystem = self.app.settings['union-filesystem']
+ morphlib.fsutils.overlay_mount(self.app.runcmd,
+ 'overlay-deploy-%s' %
+ artifact.name,
+ deploy_tree, system_tree,
+ overlay_dir, work_dir,
+ union_filesystem)
self.app.status(
msg='Writing deployment metadata file')
metadata = self.create_metadata(
artifact, root_repo_dir, deployment_type, location, env)
metadata_path = os.path.join(
- system_tree, 'baserock', 'deployment.meta')
+ deploy_tree, 'baserock', 'deployment.meta')
with morphlib.savefile.SaveFile(metadata_path, 'w') as f:
json.dump(metadata, f, indent=4,
sort_keys=True, encoding='unicode-escape')
- return system_tree
+ return deploy_tree
except Exception:
- shutil.rmtree(system_tree)
+ if deploy_tree and os.path.exists(deploy_tree):
+ morphlib.fsutils.unmount(self.app.runcmd, deploy_tree)
+ shutil.rmtree(deployment_dir)
raise
def run_deploy_commands(self, deploy_tempdir, env, artifact, root_repo_dir,
diff --git a/morphlib/plugins/distbuild_plugin.py b/morphlib/plugins/distbuild_plugin.py
index 8aaead10..708ffee1 100644
--- a/morphlib/plugins/distbuild_plugin.py
+++ b/morphlib/plugins/distbuild_plugin.py
@@ -15,10 +15,13 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
-import cliapp
import logging
import re
import sys
+import urllib2
+import urlparse
+
+import cliapp
import morphlib
import distbuild
@@ -26,6 +29,13 @@ import distbuild
group_distbuild = 'Distributed Build Options'
+
+class OutdatedCacheServerError(morphlib.Error):
+ def __init__(self):
+ self.msg = 'Writeable cache server is using an outdated version of ' \
+ 'morph-cache-server which is incompatible with this ' \
+ 'version of morph.'
+
class DistbuildOptionsPlugin(cliapp.Plugin):
def enable(self):
@@ -291,6 +301,12 @@ class ControllerDaemon(cliapp.Plugin):
self.app.settings['worker-cache-server-port']
morph_instance = self.app.settings['morph-instance']
+ request_url = urlparse.urljoin(writeable_cache_server, '/1.0/method')
+ try:
+ req = urllib2.urlopen(request_url)
+ except urllib2.URLError:
+ raise OutdatedCacheServerError()
+
listener_specs = [
# address, port, class to initiate on connection, class init args
('controller-helper-address', 'controller-helper-port',
diff --git a/morphlib/plugins/gc_plugin.py b/morphlib/plugins/gc_plugin.py
index 71522b04..54c1b43e 100644
--- a/morphlib/plugins/gc_plugin.py
+++ b/morphlib/plugins/gc_plugin.py
@@ -125,8 +125,8 @@ class GCPlugin(cliapp.Plugin):
'sufficient space already cleared',
chatty=True)
return
- lac = morphlib.localartifactcache.LocalArtifactCache(
- fs.osfs.OSFS(os.path.join(cache_path, 'artifacts')))
+ lac = morphlib.ostreeartifactcache.OSTreeArtifactCache(
+ os.path.join(cache_path, 'artifacts'))
max_age, min_age = self.calculate_delete_range()
logging.debug('Must remove artifacts older than timestamp %d'
% max_age)
@@ -144,6 +144,8 @@ class GCPlugin(cliapp.Plugin):
lac.remove(cachekey)
removed += 1
+ lac.prune()
+
# Maybe remove remaining middle-aged artifacts
for cachekey in may_delete:
if sufficient_free():
@@ -155,6 +157,7 @@ class GCPlugin(cliapp.Plugin):
self.app.status(msg='Removing source %(cachekey)s',
cachekey=cachekey, chatty=True)
lac.remove(cachekey)
+ lac.prune()
removed += 1
if sufficient_free():
diff --git a/morphlib/plugins/ostree_artifacts_plugin.py b/morphlib/plugins/ostree_artifacts_plugin.py
new file mode 100644
index 00000000..69d66dd1
--- /dev/null
+++ b/morphlib/plugins/ostree_artifacts_plugin.py
@@ -0,0 +1,169 @@
+# Copyright (C) 2015 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, see <http://www.gnu.org/licenses/>.
+
+
+import collections
+import fs
+import os
+
+import cliapp
+
+import morphlib
+from morphlib.artifactcachereference import ArtifactCacheReference
+
+
+class NoCacheError(morphlib.Error):
+
+ def __init__(self, cachedir):
+ self.msg = ("Expected artifact cache directory %s doesn't exist.\n"
+ "No existing cache to convert!" % cachedir)
+
+
+class ComponentNotInSystemError(morphlib.Error):
+
+ def __init__(self, components, system):
+ components = ', '.join(components)
+ self.msg = ('Components %s are not in %s. Ensure you provided '
+ 'component names rather than filenames.'
+ % (components, system))
+
+
+class OSTreeArtifactsPlugin(cliapp.Plugin):
+
+ def enable(self):
+ self.app.add_subcommand('convert-local-cache', self.convert_cache,
+ arg_synopsis='[DELETE]')
+ self.app.add_subcommand('query-cache', self.query_cache,
+ arg_synopsis='SYSTEM NAME...')
+
+ def disable(self):
+ pass
+
+ def convert_cache(self, args):
+ """Convert a local tarball cache into an OSTree cache.
+
+ Command line arguments:
+
+ * DELETE: This is an optional argument, which if given as "delete"
+ will cause tarball artifacts to be removed once they are converted.
+
+ This command will extract all the tarball artifacts in your local
+ artifact cache and store them in an OSTree repository in that
+ artifact cache. This will be quicker than redownloading all that
+ content from a remote cache server, but may still be time consuming
+ if your cache is large.
+
+ """
+ delete = False
+ if args:
+ if args[0] == 'delete':
+ delete = True
+
+ artifact_cachedir = os.path.join(self.app.settings['cachedir'],
+ 'artifacts')
+ if not os.path.exists(artifact_cachedir):
+ raise NoCacheError(artifact_cachedir)
+
+ tarball_cache = morphlib.localartifactcache.LocalArtifactCache(
+ fs.osfs.OSFS(artifact_cachedir))
+ ostree_cache = morphlib.ostreeartifactcache.OSTreeArtifactCache(
+ artifact_cachedir, mode=self.app.settings['ostree-repo-mode'],
+ status_cb=self.app.status)
+
+ cached_artifacts = []
+ for cachekey, artifacts, last_used in tarball_cache.list_contents():
+ for artifact in artifacts:
+ basename = '.'.join((cachekey.lstrip('/'), artifact))
+ cached_artifacts.append(ArtifactCacheReference(basename))
+
+ # Set the method property of the tarball cache to allow us to
+ # treat it like a RemoteArtifactCache.
+ tarball_cache.method = 'tarball'
+
+ to_convert = [artifact for artifact in cached_artifacts
+ if '.stratum.' not in artifact.basename()]
+ for i, artifact in enumerate(to_convert):
+ if not ostree_cache.has(artifact):
+ try:
+ cache_key, kind, name = artifact.basename().split('.', 2)
+ except ValueError:
+ # We must have metadata, which doesn't need converting
+ continue
+ self.app.status(msg='[%(current)d/%(total)d] Converting '
+ '%(name)s', current=i+1,
+ total=len(to_convert),
+ name=artifact.basename())
+ ostree_cache.copy_from_remote(artifact, tarball_cache)
+ if delete:
+ os.remove(tarball_cache.artifact_filename(artifact))
+
+ def _find_artifacts(self, names, root_artifact):
+ found = collections.OrderedDict()
+ not_found = list(names)
+ for a in root_artifact.walk():
+ name = a.source.morphology['name']
+ if name in names and name not in found:
+ found[name] = [a]
+ if name in not_found:
+ not_found.remove(name)
+ elif name in names:
+ found[name].append(a)
+ if name in not_found:
+ not_found.remove(name)
+ return found, not_found
+
+ def query_cache(self, args):
+ """Check if the cache contains an artifact.
+
+ Command line arguments:
+
+ * `SYSTEM` is the filename of the system containing the components
+ to be looked for.
+ * `NAME...` is the name of one or more components to look for.
+
+ """
+ if not args:
+ raise cliapp.AppException('You must provide at least a system '
+ 'filename.\nUsage: `morph query-cache '
+ 'SYSTEM [NAME...]`')
+ ws = morphlib.workspace.open('.')
+ sb = morphlib.sysbranchdir.open_from_within('.')
+
+ system_filename = morphlib.util.sanitise_morphology_path(args[0])
+ system_filename = sb.relative_to_root_repo(system_filename)
+ component_names = args[1:]
+
+ bc = morphlib.buildcommand.BuildCommand(self.app)
+ repo = sb.get_config('branch.root')
+ ref = sb.get_config('branch.name')
+
+ definitions_repo_path = sb.get_git_directory_name(repo)
+ definitions_repo = morphlib.gitdir.GitDirectory(definitions_repo_path)
+ commit = definitions_repo.resolve_ref_to_commit(ref)
+
+ srcpool = bc.create_source_pool(repo, commit, system_filename)
+ bc.validate_sources(srcpool)
+ root = bc.resolve_artifacts(srcpool)
+ if not component_names:
+ component_names = [root.source.name]
+ components, not_found = self._find_artifacts(component_names, root)
+ if not_found:
+ raise ComponentNotInSystemError(not_found, system_filename)
+
+ for name, artifacts in components.iteritems():
+ for component in artifacts:
+ if bc.lac.has(component):
+ print bc.lac._get_artifact_cache_name(component)
+ else:
+ print '%s is not cached' % name
diff --git a/morphlib/remoteartifactcache.py b/morphlib/remoteartifactcache.py
index 427e4cbb..1a1b5404 100644
--- a/morphlib/remoteartifactcache.py
+++ b/morphlib/remoteartifactcache.py
@@ -57,6 +57,18 @@ class RemoteArtifactCache(object):
def __init__(self, server_url):
self.server_url = server_url
+ self.name = urlparse.urlparse(server_url).hostname
+ try:
+ self.method = self._get_method()
+ except urllib2.URLError:
+ self.method = 'tarball'
+ except Exception as e: # pragma: no cover
+ raise cliapp.AppException(
+ 'Failed to contact remote artifact cache "%s". Error: %s.' %
+ (self.server_url, e))
+ if self.method == 'ostree': # pragma: no cover
+ self.ostree_url = 'http://%s:%s/' % (self.name,
+ self._get_ostree_info())
def has(self, artifact):
return self._has_file(artifact.basename())
@@ -112,5 +124,18 @@ class RemoteArtifactCache(object):
server_url, '/1.0/artifacts?filename=%s' %
urllib.quote(filename))
+ def _get_method(self): # pragma: no cover
+ logging.debug('Getting cache method of %s' % self.server_url)
+ request_url = urlparse.urljoin(self.server_url, '/1.0/method')
+ req = urllib2.urlopen(request_url)
+ return req.read()
+
+ def _get_ostree_info(self): # pragma: no cover
+ logging.debug('Getting OSTree repo info.')
+ request_url = urlparse.urljoin(self.server_url, '/1.0/ostreeinfo')
+ logging.debug('sending %s' % request_url)
+ req = urllib2.urlopen(request_url)
+ return req.read()
+
def __str__(self): # pragma: no cover
return self.server_url
diff --git a/morphlib/stagingarea.py b/morphlib/stagingarea.py
index 8c2781aa..859e7481 100644
--- a/morphlib/stagingarea.py
+++ b/morphlib/stagingarea.py
@@ -87,6 +87,14 @@ class StagingArea(object):
return self._dir_for_source(source, 'inst')
+ def overlay_upperdir(self, source):
+ '''Create a directory to be upperdir for overlayfs, and return it.'''
+ return self._dir_for_source(source, 'overlay_upper')
+
+ def overlaydir(self, source):
+ '''Create a directory to be a mount point for overlayfs, return it'''
+ return self._dir_for_source(source, 'overlay')
+
def relative(self, filename):
'''Return a filename relative to the staging area.'''
@@ -100,83 +108,14 @@ class StagingArea(object):
assert filename.startswith(dirname)
return filename[len(dirname) - 1:] # include leading slash
- def hardlink_all_files(self, srcpath, destpath): # pragma: no cover
- '''Hardlink every file in the path to the staging-area
-
- If an exception is raised, the staging-area is indeterminate.
-
- '''
-
- file_stat = os.lstat(srcpath)
- mode = file_stat.st_mode
-
- if stat.S_ISDIR(mode):
- # Ensure directory exists in destination, then recurse.
- if not os.path.lexists(destpath):
- os.makedirs(destpath)
- dest_stat = os.stat(os.path.realpath(destpath))
- if not stat.S_ISDIR(dest_stat.st_mode):
- raise IOError('Destination not a directory. source has %s'
- ' destination has %s' % (srcpath, destpath))
-
- for entry in os.listdir(srcpath):
- self.hardlink_all_files(os.path.join(srcpath, entry),
- os.path.join(destpath, entry))
- elif stat.S_ISLNK(mode):
- # Copy the symlink.
- if os.path.lexists(destpath):
- os.remove(destpath)
- os.symlink(os.readlink(srcpath), destpath)
-
- elif stat.S_ISREG(mode):
- # Hardlink the file.
- if os.path.lexists(destpath):
- os.remove(destpath)
- os.link(srcpath, destpath)
-
- elif stat.S_ISCHR(mode) or stat.S_ISBLK(mode):
- # Block or character device. Put contents of st_dev in a mknod.
- if os.path.lexists(destpath):
- os.remove(destpath)
- os.mknod(destpath, file_stat.st_mode, file_stat.st_rdev)
- os.chmod(destpath, file_stat.st_mode)
-
- else:
- # Unsupported type.
- raise IOError('Cannot extract %s into staging-area. Unsupported'
- ' type.' % srcpath)
-
- def install_artifact(self, handle):
- '''Install a build artifact into the staging area.
-
- We access the artifact via an open file handle. For now, we assume
- the artifact is a tarball.
-
- '''
-
- chunk_cache_dir = os.path.join(self._app.settings['tempdir'], 'chunks')
- unpacked_artifact = os.path.join(
- chunk_cache_dir, os.path.basename(handle.name) + '.d')
- if not os.path.exists(unpacked_artifact):
- self._app.status(
- msg='Unpacking chunk from cache %(filename)s',
- filename=os.path.basename(handle.name))
- savedir = tempfile.mkdtemp(dir=chunk_cache_dir)
- try:
- morphlib.bins.unpack_binary_from_file(
- handle, savedir + '/')
- except BaseException as e: # pragma: no cover
- shutil.rmtree(savedir)
- raise
- # TODO: This rename is not concurrency safe if two builds are
- # extracting the same chunk, one build will fail because
- # the other renamed its tempdir here first.
- os.rename(savedir, unpacked_artifact)
-
+ def install_artifact(self, artifact_cache, artifact):
+ '''Install a build artifact into the staging area.'''
if not os.path.exists(self.dirname):
self._mkdir(self.dirname)
- self.hardlink_all_files(unpacked_artifact, self.dirname)
+ artifact_cache.get(artifact, directory=self.dirname)
+
+ morphlib.util.create_devices(artifact.source.morphology, self.dirname)
def remove(self):
'''Remove the entire staging area.
diff --git a/morphlib/stagingarea_tests.py b/morphlib/stagingarea_tests.py
index 97d78236..3d378573 100644
--- a/morphlib/stagingarea_tests.py
+++ b/morphlib/stagingarea_tests.py
@@ -30,6 +30,7 @@ class FakeBuildEnvironment(object):
}
self.extra_path = ['/extra-path']
+
class FakeSource(object):
def __init__(self):
@@ -39,6 +40,31 @@ class FakeSource(object):
self.name = 'le-name'
+class FakeArtifact(object):
+
+ def __init__(self):
+ self.source = FakeSource()
+
+
+class FakeArtifactCache(object):
+
+ def __init__(self, tempdir):
+ self.tempdir = tempdir
+
+ def create_chunk(self, chunkdir):
+ if not chunkdir:
+ chunkdir = os.path.join(self.tempdir, 'chunk')
+ if not os.path.exists(chunkdir):
+ os.mkdir(chunkdir)
+ with open(os.path.join(chunkdir, 'file.txt'), 'w'):
+ pass
+
+ return chunkdir
+
+ def get(self, artifact, directory=None):
+ return self.create_chunk(directory)
+
+
class FakeApplication(object):
def __init__(self, cachedir, tempdir):
@@ -83,12 +109,8 @@ class StagingAreaTests(unittest.TestCase):
os.mkdir(chunkdir)
with open(os.path.join(chunkdir, 'file.txt'), 'w'):
pass
- chunk_tar = os.path.join(self.tempdir, 'chunk.tar')
- tf = tarfile.TarFile(name=chunk_tar, mode='w')
- tf.add(chunkdir, arcname='.')
- tf.close()
- return chunk_tar
+ return chunkdir
def list_tree(self, root):
files = []
@@ -118,20 +140,34 @@ class StagingAreaTests(unittest.TestCase):
self.assertEqual(self.created_dirs, [dirname])
self.assertTrue(dirname.startswith(self.staging))
+ def test_creates_overlay_upper_directory(self):
+ source = FakeSource()
+ self.sa._mkdir = self.fake_mkdir
+ dirname = self.sa.overlay_upperdir(source)
+ self.assertEqual(self.created_dirs, [dirname])
+ self.assertTrue(dirname.startswith(self.staging))
+
+ def test_creates_overlay_directory(self):
+ source = FakeSource()
+ self.sa._mkdir = self.fake_mkdir
+ dirname = self.sa.overlaydir(source)
+ self.assertEqual(self.created_dirs, [dirname])
+ self.assertTrue(dirname.startswith(self.staging))
+
def test_makes_relative_name(self):
filename = os.path.join(self.staging, 'foobar')
self.assertEqual(self.sa.relative(filename), '/foobar')
def test_installs_artifact(self):
- chunk_tar = self.create_chunk()
- with open(chunk_tar, 'rb') as f:
- self.sa.install_artifact(f)
+ artifact = FakeArtifact()
+ artifact_cache = FakeArtifactCache(self.tempdir)
+ self.sa.install_artifact(artifact_cache, artifact)
self.assertEqual(self.list_tree(self.staging), ['/', '/file.txt'])
def test_removes_everything(self):
- chunk_tar = self.create_chunk()
- with open(chunk_tar, 'rb') as f:
- self.sa.install_artifact(f)
+ artifact = FakeArtifact()
+ artifact_cache = FakeArtifactCache(self.tempdir)
+ self.sa.install_artifact(artifact_cache, artifact)
self.sa.remove()
self.assertFalse(os.path.exists(self.staging))
diff --git a/morphlib/util.py b/morphlib/util.py
index 904dc355..8179e523 100644
--- a/morphlib/util.py
+++ b/morphlib/util.py
@@ -14,16 +14,21 @@
import contextlib
import itertools
+import json
+import logging
import os
import pipes
import re
+import stat
import subprocess
-import textwrap
import sys
+import textwrap
+import cliapp
import fs.osfs
import morphlib
+from morphlib.artifactcachereference import ArtifactCacheReference
'''Utility functions for morph.'''
@@ -120,7 +125,7 @@ def get_git_resolve_cache_server(settings): # pragma: no cover
return None
-def new_artifact_caches(settings): # pragma: no cover
+def new_artifact_caches(settings, status_cb=None): # pragma: no cover
'''Create new objects for local and remote artifact caches.
This includes creating the directories on disk, if missing.
@@ -132,12 +137,17 @@ def new_artifact_caches(settings): # pragma: no cover
if not os.path.exists(artifact_cachedir):
os.mkdir(artifact_cachedir)
- lac = morphlib.localartifactcache.LocalArtifactCache(
- fs.osfs.OSFS(artifact_cachedir))
+ mode = settings['ostree-repo-mode']
+ lac = morphlib.ostreeartifactcache.OSTreeArtifactCache(
+ artifact_cachedir, mode=mode, status_cb=status_cb)
rac_url = get_artifact_cache_server(settings)
rac = None
- if rac_url:
+ # We let 'none' here specify 'don't use a remote artifact cache'.
+ # The 'artifact-cache-server' setting defaults to a calue based
+ # on 'trove-host' so it's not enough to specify --artifact-cache-server=''
+ # if you want to force Morph to ignore cached artifacts.
+ if rac_url and rac_url.lower() != 'none':
rac = morphlib.remoteartifactcache.RemoteArtifactCache(rac_url)
return lac, rac
@@ -691,3 +701,47 @@ def write_from_dict(filepath, d, validate=lambda x, y: True): #pragma: no cover
os.fchown(f.fileno(), 0, 0)
os.fchmod(f.fileno(), 0644)
+
+
+def create_devices(morphology, destdir): # pragma: no cover
+ '''Creates device nodes if the morphology specifies them'''
+ perms_mask = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
+ if 'devices' in morphology and morphology['devices'] is not None:
+ for dev in morphology['devices']:
+ destfile = os.path.join(destdir, './' + dev['filename'])
+ mode = int(dev['permissions'], 8) & perms_mask
+ if dev['type'] == 'c':
+ mode = mode | stat.S_IFCHR
+ elif dev['type'] == 'b':
+ mode = mode | stat.S_IFBLK
+ else:
+ raise IOError('Cannot create device node %s,'
+ 'unrecognized device type "%s"'
+ % (destfile, dev['type']))
+ parent = os.path.dirname(destfile)
+ if not os.path.exists(parent):
+ os.makedirs(parent)
+ if not os.path.exists(destfile):
+ logging.debug("Creating device node %s" % destfile)
+ os.mknod(destfile, mode,
+ os.makedev(dev['major'], dev['minor']))
+ os.chown(destfile, dev['uid'], dev['gid'])
+
+
+def get_stratum_contents(cache, stratum_artifact): # pragma: no cover
+ '''Load a stratum from a local artifact cache.
+
+ Returns a list of ArtifactCacheReference instances for the chunks
+ contained in the stratum.
+
+ '''
+
+ with open(cache.get(stratum_artifact), 'r') as stratum_file:
+ try:
+ artifact_list = json.load(stratum_file,
+ encoding='unicode-escape')
+ except ValueError as e:
+ raise cliapp.AppException(
+ 'Corruption detected: %s while loading %s' %
+ (e, cache.artifact_filename(stratum_artifact)))
+ return [ArtifactCacheReference(a) for a in artifact_list]