summaryrefslogtreecommitdiff
path: root/morphlib/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'morphlib/plugins')
-rw-r--r--morphlib/plugins/add_binary_plugin.py110
-rw-r--r--morphlib/plugins/branch_and_merge_new_plugin.py14
-rw-r--r--morphlib/plugins/deploy_plugin.py256
-rw-r--r--morphlib/plugins/push_pull_plugin.py93
4 files changed, 402 insertions, 71 deletions
diff --git a/morphlib/plugins/add_binary_plugin.py b/morphlib/plugins/add_binary_plugin.py
new file mode 100644
index 00000000..1edae0e8
--- /dev/null
+++ b/morphlib/plugins/add_binary_plugin.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2014 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 logging
+import os
+import urlparse
+
+import morphlib
+
+
+class AddBinaryPlugin(cliapp.Plugin):
+
+ '''Add a subcommand for dealing with large binary files.'''
+
+ def enable(self):
+ self.app.add_subcommand(
+ 'add-binary', self.add_binary, arg_synopsis='FILENAME...')
+
+ def disable(self):
+ pass
+
+ def add_binary(self, binaries):
+ '''Add a binary file to the current repository.
+
+ Command line argument:
+
+ * `FILENAME...` is the binaries to be added to the repository.
+
+ This checks for the existence of a .gitfat file in the repository. If
+ there is one then a line is added to .gitattributes telling it that
+ the given binary should be handled by git-fat. If there is no .gitfat
+ file then it is created, with the rsync remote pointing at the correct
+ directory on the Trove host. A line is then added to .gitattributes to
+ say that the given binary should be handled by git-fat.
+
+ Example:
+
+ morph add-binary big_binary.tar.gz
+
+ '''
+ if not binaries:
+ raise morphlib.Error('add-binary must get at least one argument')
+
+ gd = morphlib.gitdir.GitDirectory(os.getcwd())
+ gd.fat_init()
+ if not gd.has_fat():
+ self._make_gitfat(gd)
+ self._handle_binaries(binaries, gd)
+ logging.info('Staged binaries for commit')
+
+ def _handle_binaries(self, binaries, gd):
+ '''Add a filter for the given file, and then add it to the repo.'''
+ # begin by ensuring all paths given are relative to the root directory
+ files = [gd.get_relpath(os.path.realpath(binary))
+ for binary in binaries]
+
+ # now add any files that aren't already mentioned in .gitattributes to
+ # the file so that git fat knows what to do
+ attr_path = gd.join_path('.gitattributes')
+ if '.gitattributes' in gd.list_files():
+ with open(attr_path, 'r') as attributes:
+ current = set(f.split()[0] for f in attributes)
+ else:
+ current = set()
+ to_add = set(files) - current
+
+ # if we don't need to change .gitattributes then we can just do
+ # `git add <binaries>`
+ if not to_add:
+ gd.get_index().add_files_from_working_tree(files)
+ return
+
+ with open(attr_path, 'a') as attributes:
+ for path in to_add:
+ attributes.write('%s filter=fat -crlf\n' % path)
+
+ # we changed .gitattributes, so need to stage it for committing
+ files.append(attr_path)
+ gd.get_index().add_files_from_working_tree(files)
+
+ def _make_gitfat(self, gd):
+ '''Make .gitfat point to the rsync directory for the repo.'''
+ remote = gd.get_remote('origin')
+ if not remote.get_push_url():
+ raise Exception(
+ 'Remote `origin` does not have a push URL defined.')
+ url = urlparse.urlparse(remote.get_push_url())
+ if url.scheme != 'ssh':
+ raise Exception(
+ 'Push URL for `origin` is not an SSH URL: %s' % url.geturl())
+ fat_store = '%s:%s' % (url.netloc, url.path)
+ fat_path = gd.join_path('.gitfat')
+ with open(fat_path, 'w+') as gitfat:
+ gitfat.write('[rsync]\n')
+ gitfat.write('remote = %s' % fat_store)
+ gd.get_index().add_files_from_working_tree([fat_path])
diff --git a/morphlib/plugins/branch_and_merge_new_plugin.py b/morphlib/plugins/branch_and_merge_new_plugin.py
index 94b2381c..51cba401 100644
--- a/morphlib/plugins/branch_and_merge_new_plugin.py
+++ b/morphlib/plugins/branch_and_merge_new_plugin.py
@@ -190,8 +190,12 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin):
with self._initializing_system_branch(
ws, root_url, system_branch, cached_repo, base_ref) as (sb, gd):
+ if gd.has_fat():
+ gd.fat_init()
+ gd.fat_pull()
+
if not self._checkout_has_systems(gd):
- raise BranchRootHasNoSystemsError(base_ref)
+ raise BranchRootHasNoSystemsError(root_url, base_ref)
def branch(self, args):
@@ -250,9 +254,12 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin):
gd.branch(system_branch, base_ref)
gd.checkout(system_branch)
+ if gd.has_fat():
+ gd.fat_init()
+ gd.fat_pull()
if not self._checkout_has_systems(gd):
- raise BranchRootHasNoSystemsError(base_ref)
+ raise BranchRootHasNoSystemsError(root_url, base_ref)
def _save_dirty_morphologies(self, loader, sb, morphs):
logging.debug('Saving dirty morphologies: start')
@@ -480,6 +487,9 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin):
gd.checkout(sb.system_branch_name)
gd.update_submodules(self.app)
gd.update_remotes()
+ if gd.has_fat():
+ gd.fat_init()
+ gd.fat_pull()
# Change the refs to the chunk.
if chunk_ref != sb.system_branch_name:
diff --git a/morphlib/plugins/deploy_plugin.py b/morphlib/plugins/deploy_plugin.py
index 90c658a0..ae62b75d 100644
--- a/morphlib/plugins/deploy_plugin.py
+++ b/morphlib/plugins/deploy_plugin.py
@@ -16,6 +16,7 @@
import cliapp
import contextlib
+import json
import os
import shutil
import stat
@@ -25,23 +26,24 @@ import uuid
import morphlib
-# UGLY HACK: We need to re-use some code from the branch and merge
-# plugin, so we import and instantiate that plugin. This needs to
-# be fixed by refactoring the codebase so the shared code is in
-# morphlib, not in a plugin. However, this hack lets us re-use
-# code without copying it.
-import morphlib.plugins.branch_and_merge_plugin
+
+class ExtensionNotFoundError(morphlib.Error):
+ pass
class DeployPlugin(cliapp.Plugin):
def enable(self):
+ group_deploy = 'Deploy Options'
+ self.app.settings.boolean(['upgrade'],
+ 'specify that you want to upgrade an '
+ 'existing cluster of systems rather than do '
+ 'an initial deployment',
+ group=group_deploy)
+
self.app.add_subcommand(
'deploy', self.deploy,
arg_synopsis='CLUSTER [SYSTEM.KEY=VALUE]')
- self.other = \
- morphlib.plugins.branch_and_merge_plugin.BranchAndMergePlugin()
- self.other.app = self.app
def disable(self):
pass
@@ -250,8 +252,20 @@ class DeployPlugin(cliapp.Plugin):
are set as environment variables when either the configuration or the
write extension runs (except `type` and `location`).
+ Deployment configuration is stored in the deployed system as
+ /baserock/deployment.meta. THIS CONTAINS ALL ENVIRONMENT VARIABLES SET
+ DURINGR DEPLOYMENT, so make sure you have no sensitive information in
+ your environment that is being leaked. As a special case, any
+ environment/deployment variable that contains 'PASSWORD' in its name is
+ stripped out and not stored in the final system.
+
'''
+ # Nasty hack to allow deploying things of a different architecture
+ def validate(self, root_artifact):
+ pass
+ morphlib.buildcommand.BuildCommand._validate_architecture = validate
+
if not args:
raise cliapp.AppException(
'Too few arguments to deploy command (see help)')
@@ -318,65 +332,112 @@ class DeployPlugin(cliapp.Plugin):
ref=build_ref, dirname=gd.dirname,
remote=remote.get_push_url(), chatty=True)
- for system in cluster_morphology['systems']:
- self.deploy_system(build_command, root_repo_dir,
- bb.root_repo_url, bb.root_ref,
- system, env_vars)
-
- def deploy_system(self, build_command, root_repo_dir, build_repo, ref,
- system, env_vars):
- # Find the artifact to build
- morph = system['morph']
- srcpool = build_command.create_source_pool(build_repo, ref,
- morph + '.morph')
- def validate(self, root_artifact):
- pass
- morphlib.buildcommand.BuildCommand._validate_architecture = validate
+ # Create a tempdir for this deployment to work in
+ deploy_tempdir = tempfile.mkdtemp(
+ dir=os.path.join(self.app.settings['tempdir'], 'deployments'))
+ try:
+ for system in cluster_morphology['systems']:
+ self.deploy_system(build_command, deploy_tempdir,
+ root_repo_dir, bb.root_repo_url,
+ bb.root_ref, system, env_vars,
+ parent_location='')
+ finally:
+ shutil.rmtree(deploy_tempdir)
+
+ self.app.status(msg='Finished deployment')
- artifact = build_command.resolve_artifacts(srcpool)
-
- deploy_defaults = system['deploy-defaults']
- deployments = system['deploy']
- for system_id, deploy_params in deployments.iteritems():
- user_env = morphlib.util.parse_environment_pairs(
- os.environ,
- [pair[len(system_id)+1:]
- for pair in env_vars
- if pair.startswith(system_id)])
-
- final_env = dict(deploy_defaults.items() +
- deploy_params.items() +
- user_env.items())
-
- deployment_type = final_env.pop('type', None)
- if not deployment_type:
- raise morphlib.Error('"type" is undefined '
- 'for system "%s"' % system_id)
-
- location = final_env.pop('location', None)
- if not location:
- raise morphlib.Error('"location" is undefined '
- 'for system "%s"' % system_id)
-
- morphlib.util.sanitize_environment(final_env)
- self.do_deploy(build_command, root_repo_dir, ref, artifact,
- deployment_type, location, final_env)
-
- def do_deploy(self, build_command, root_repo_dir, ref, artifact,
- deployment_type, location, env):
-
- # Create a tempdir for this deployment to work in
- deploy_tempdir = tempfile.mkdtemp(
- dir=os.path.join(self.app.settings['tempdir'], 'deployments'))
+ def deploy_system(self, build_command, deploy_tempdir,
+ root_repo_dir, build_repo, ref, system, env_vars,
+ parent_location):
+ old_status_prefix = self.app.status_prefix
+ system_status_prefix = '%s[%s]' % (old_status_prefix, system['morph'])
+ self.app.status_prefix = system_status_prefix
try:
- # Create a tempdir to extract the rootfs in
- system_tree = tempfile.mkdtemp(dir=deploy_tempdir)
+ # Find the artifact to build
+ morph = system['morph']
+ srcpool = build_command.create_source_pool(build_repo, ref,
+ morph + '.morph')
+
+ artifact = build_command.resolve_artifacts(srcpool)
+
+ deploy_defaults = system.get('deploy-defaults', {})
+ deployments = system['deploy']
+ for system_id, deploy_params in deployments.iteritems():
+ deployment_status_prefix = '%s[%s]' % (
+ system_status_prefix, system_id)
+ self.app.status_prefix = deployment_status_prefix
+ try:
+ user_env = morphlib.util.parse_environment_pairs(
+ os.environ,
+ [pair[len(system_id)+1:]
+ for pair in env_vars
+ if pair.startswith(system_id)])
+
+ final_env = dict(deploy_defaults.items() +
+ deploy_params.items() +
+ user_env.items())
+
+ is_upgrade = ('yes' if self.app.settings['upgrade']
+ else 'no')
+ final_env['UPGRADE'] = is_upgrade
+
+ deployment_type = final_env.pop('type', None)
+ if not deployment_type:
+ raise morphlib.Error('"type" is undefined '
+ 'for system "%s"' % system_id)
+
+ location = final_env.pop('location', None)
+ if not location:
+ raise morphlib.Error('"location" is undefined '
+ 'for system "%s"' % system_id)
+
+ morphlib.util.sanitize_environment(final_env)
+ self.check_deploy(root_repo_dir, ref, deployment_type,
+ location, final_env)
+ system_tree = self.setup_deploy(build_command,
+ deploy_tempdir,
+ root_repo_dir,
+ ref, artifact,
+ deployment_type,
+ location, final_env)
+ for subsystem in system.get('subsystems', []):
+ self.deploy_system(build_command, deploy_tempdir,
+ root_repo_dir, build_repo,
+ ref, subsystem, env_vars,
+ parent_location=system_tree)
+ if parent_location:
+ deploy_location = os.path.join(parent_location,
+ location.lstrip('/'))
+ else:
+ deploy_location = location
+ self.run_deploy_commands(deploy_tempdir, final_env,
+ artifact, root_repo_dir,
+ ref, deployment_type,
+ system_tree, deploy_location)
+ finally:
+ self.app.status_prefix = system_status_prefix
+ finally:
+ self.app.status_prefix = old_status_prefix
- # Extensions get a private tempdir so we can more easily clean
- # up any files an extension left behind
- deploy_private_tempdir = tempfile.mkdtemp(dir=deploy_tempdir)
- env['TMPDIR'] = deploy_private_tempdir
+ def check_deploy(self, root_repo_dir, ref, deployment_type, location, env):
+ # Run optional write check extension. These are separate from the write
+ # extension because it may be several minutes before the write
+ # extension itself has the chance to raise an error.
+ try:
+ self._run_extension(
+ root_repo_dir, ref, deployment_type, '.check',
+ [location], env)
+ except ExtensionNotFoundError:
+ pass
+
+ def setup_deploy(self, build_command, deploy_tempdir, root_repo_dir, ref,
+ artifact, deployment_type, location, env):
+ # 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)
+
+ try:
# Unpack the artifact (tarball) to a temporary directory.
self.app.status(msg='Unpacking system for configuration')
@@ -397,7 +458,27 @@ class DeployPlugin(cliapp.Plugin):
msg='System unpacked at %(system_tree)s',
system_tree=system_tree)
+ 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')
+ with morphlib.savefile.SaveFile(metadata_path, 'w') as f:
+ f.write(json.dumps(metadata, indent=4, sort_keys=True))
+ return system_tree
+ except Exception:
+ shutil.rmtree(system_tree)
+ raise
+
+ def run_deploy_commands(self, deploy_tempdir, env, artifact, root_repo_dir,
+ ref, deployment_type, system_tree, location):
+ # Extensions get a private tempdir so we can more easily clean
+ # up any files an extension left behind
+ deploy_private_tempdir = tempfile.mkdtemp(dir=deploy_tempdir)
+ env['TMPDIR'] = deploy_private_tempdir
+ try:
# Run configuration extensions.
self.app.status(msg='Configure system')
names = artifact.source.morphology['configuration-extensions']
@@ -423,9 +504,7 @@ class DeployPlugin(cliapp.Plugin):
finally:
# Cleanup.
self.app.status(msg='Cleaning up')
- shutil.rmtree(deploy_tempdir)
-
- self.app.status(msg='Finished deployment')
+ shutil.rmtree(deploy_private_tempdir)
def _run_extension(self, gd, ref, name, kind, args, env):
'''Run an extension.
@@ -446,7 +525,7 @@ class DeployPlugin(cliapp.Plugin):
code_dir = os.path.dirname(morphlib.__file__)
ext_filename = os.path.join(code_dir, 'exts', name + kind)
if not os.path.exists(ext_filename):
- raise morphlib.Error(
+ raise ExtensionNotFoundError(
'Could not find extension %s%s' % (name, kind))
if not self._is_executable(ext_filename):
raise morphlib.Error(
@@ -464,7 +543,8 @@ class DeployPlugin(cliapp.Plugin):
name=name, kind=kind)
self.app.runcmd(
[ext_filename] + args,
- ['sh', '-c', 'while read l; do echo `date "+%F %T"` $l; done'],
+ ['sh', '-c', 'while read l; do echo `date "+%F %T"` "$1$l"; done',
+ '-', '%s[%s]' % (self.app.status_prefix, name + kind)],
cwd=gd.dirname, env=env, stdout=None, stderr=None)
if delete_ext:
@@ -474,3 +554,41 @@ class DeployPlugin(cliapp.Plugin):
st = os.stat(filename)
mask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
return (stat.S_IMODE(st.st_mode) & mask) != 0
+
+ def create_metadata(self, system_artifact, root_repo_dir, deployment_type,
+ location, env):
+ '''Deployment-specific metadata.
+
+ The `build` and `deploy` operations must be from the same ref, so full
+ info on the root repo that the system came from is in
+ /baserock/${system_artifact}.meta and is not duplicated here. We do
+ store a `git describe` of the definitions.git repo as a convenience for
+ post-upgrade hooks that we may need to implement at a future date:
+ the `git describe` output lists the last tag, which will hopefully help
+ us to identify which release of a system was deployed without having to
+ keep a list of SHA1s somewhere or query a Trove.
+
+ '''
+
+ def remove_passwords(env):
+ def is_password(key):
+ return 'PASSWORD' in key
+ return { k:v for k, v in env.iteritems() if not is_password(k) }
+
+ meta = {
+ 'system-artifact-name': system_artifact.name,
+ 'configuration': remove_passwords(env),
+ 'deployment-type': deployment_type,
+ 'location': location,
+ 'definitions-version': {
+ 'describe': root_repo_dir.describe(),
+ },
+ 'morph-version': {
+ 'ref': morphlib.gitversion.ref,
+ 'tree': morphlib.gitversion.tree,
+ 'commit': morphlib.gitversion.commit,
+ 'version': morphlib.gitversion.version,
+ },
+ }
+
+ return meta
diff --git a/morphlib/plugins/push_pull_plugin.py b/morphlib/plugins/push_pull_plugin.py
new file mode 100644
index 00000000..843de1a6
--- /dev/null
+++ b/morphlib/plugins/push_pull_plugin.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2014 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 logging
+import os
+
+import morphlib
+
+
+class PushPullPlugin(cliapp.Plugin):
+
+ '''Add subcommands to wrap the git push and pull commands.'''
+
+ def enable(self):
+ self.app.add_subcommand(
+ 'push', self.push, arg_synopsis='REPO TARGET')
+ self.app.add_subcommand('pull', self.pull, arg_synopsis='[REMOTE]')
+
+ def disable(self):
+ pass
+
+ def push(self, args):
+ '''Push a branch to a remote repository.
+
+ Command line arguments:
+
+ * `REPO` is the repository to push your changes to.
+
+ * `TARGET` is the branch to push to the repository.
+
+ This is a wrapper for the `git push` command. It also deals with
+ pushing any binary files that have been added using git-fat.
+
+ Example:
+
+ morph push origin jrandom/new-feature
+
+ '''
+ if len(args) != 2:
+ raise morphlib.Error('push must get exactly two arguments')
+
+ gd = morphlib.gitdir.GitDirectory(os.getcwd())
+ remote, branch = args
+ rs = morphlib.gitdir.RefSpec(branch)
+ gd.get_remote(remote).push(rs)
+ if gd.has_fat():
+ gd.fat_init()
+ gd.fat_push()
+
+ def pull(self, args):
+ '''Pull changes to the current branch from a repository.
+
+ Command line arguments:
+
+ * `REMOTE` is the remote branch to pull from. By default this is the
+ branch being tracked by your current git branch (ie origin/master
+ for branch master)
+
+ This is a wrapper for the `git pull` command. It also deals with
+ pulling any binary files that have been added to the repository using
+ git-fat.
+
+ Example:
+
+ morph pull
+
+ '''
+ if len(args) > 1:
+ raise morphlib.Error('pull takes at most one argument')
+
+ gd = morphlib.gitdir.GitDirectory(os.getcwd())
+ remote = gd.get_remote('origin')
+ if args:
+ branch = args[0]
+ remote.pull(branch)
+ else:
+ remote.pull()
+ if gd.has_fat():
+ gd.fat_init()
+ gd.fat_pull()