summaryrefslogtreecommitdiff
path: root/lorrycontroller/lstroves.py
diff options
context:
space:
mode:
Diffstat (limited to 'lorrycontroller/lstroves.py')
-rw-r--r--lorrycontroller/lstroves.py217
1 files changed, 217 insertions, 0 deletions
diff --git a/lorrycontroller/lstroves.py b/lorrycontroller/lstroves.py
new file mode 100644
index 0000000..e69dce2
--- /dev/null
+++ b/lorrycontroller/lstroves.py
@@ -0,0 +1,217 @@
+# 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 json
+import logging
+import time
+
+import bottle
+import cliapp
+
+import lorrycontroller
+
+
+class GitanoLsError(Exception):
+
+ def __init__(self, trovehost, output):
+ Exception.__init__(
+ self,
+ 'Failed to get list of git repositories '
+ 'on remote host %s:\n%s' % (trovehost, output))
+ self.trovehost = trovehost
+
+
+class TroveRepositoryLister(object):
+
+ def __init__(self, app_settings, route):
+ self.app_settings = app_settings
+ self.route = route
+
+ def list_trove_into_statedb(self, statedb, trove_info):
+ remote_paths = self.ls(statedb, trove_info)
+ remote_paths = self.skip_ignored_repos(trove_info, remote_paths)
+ repo_map = self.map_remote_repos_to_local_ones(
+ trove_info, remote_paths)
+
+ with statedb:
+ self.update_lorries_for_trove(statedb, trove_info, repo_map)
+ now = statedb.get_current_time()
+ statedb.set_trove_ls_last_run(trove_info['trovehost'], now)
+
+ def ls(self, statedb, trove_info):
+ if self.app_settings['debug-fake-trove']:
+ repo_paths = self.get_fake_ls_output(trove_info)
+ else:
+ repo_paths = self.get_real_ls_output(statedb, trove_info)
+
+ return repo_paths
+
+ def get_fake_ls_output(self, trove_info):
+ trovehost = trove_info['trovehost']
+ for item in self.app_settings['debug-fake-trove']:
+ host, path = item.split('=', 1)
+ if host == trovehost:
+ with open(path) as f:
+ obj = json.load(f)
+ return obj['ls-output']
+ return None
+
+ def get_real_ls_output(self, statedb, trove_info):
+ gitano = lorrycontroller.new_gitano_command(statedb, trove_info['trovehost'])
+ output = gitano.ls()
+ return self.parse_ls_output(output)
+
+ def parse_ls_output(self, ls_output):
+ repo_paths = []
+ for line in ls_output.splitlines():
+ words = line.split(None, 1)
+ if words[0].startswith('R') and len(words) == 2:
+ repo_paths.append(words[1])
+ return repo_paths
+
+ def skip_ignored_repos(self, trovehost, repo_paths):
+ ignored_paths = json.loads(trovehost['ignore'])
+ return [x for x in repo_paths if x not in ignored_paths]
+
+ def map_remote_repos_to_local_ones(self, trove_info, remote_paths):
+ '''Return a dict that maps each remote repo path to a local one.'''
+ prefixmap = self.parse_prefixmap(trove_info['prefixmap'])
+ repo_map = {}
+ for remote_path in remote_paths:
+ local_path = self.map_one_remote_repo_to_local_one(
+ remote_path, prefixmap)
+ if local_path:
+ repo_map[remote_path] = local_path
+ else:
+ logging.debug('Remote repo %r not in prefixmap', remote_path)
+ return repo_map
+
+ def parse_prefixmap(self, prefixmap_string):
+ return json.loads(prefixmap_string)
+
+ def map_one_remote_repo_to_local_one(self, remote_path, prefixmap):
+ for remote_prefix in prefixmap:
+ if self.path_starts_with_prefix(remote_path, remote_prefix):
+ local_prefix = prefixmap[remote_prefix]
+ relative_path = remote_path[len(remote_prefix):]
+ local_path = local_prefix + relative_path
+ return local_path
+ return None
+
+ def path_starts_with_prefix(self, path, prefix):
+ return path.startswith(prefix) and path[len(prefix):].startswith('/')
+
+ def update_lorries_for_trove(self, statedb, trove_info, repo_map):
+ trovehost = trove_info['trovehost']
+ for remote_path, local_path in repo_map.items():
+ lorry = self.construct_lorry(trove_info, local_path, remote_path)
+ statedb.add_to_lorries(
+ path=local_path,
+ text=json.dumps(lorry, indent=4),
+ from_trovehost=trovehost,
+ from_path=remote_path,
+ interval=trove_info['lorry_interval'],
+ timeout=trove_info['lorry_timeout'])
+
+ all_local_paths = set(statedb.get_lorries_for_trove(trovehost))
+ wanted_local_paths = set(repo_map.values())
+ delete_local_paths = all_local_paths.difference(wanted_local_paths)
+ for local_path in delete_local_paths:
+ statedb.remove_lorry(local_path)
+
+ def construct_lorry(self, trove_info, local_path, remote_path):
+ return {
+ local_path: {
+ 'type': 'git',
+ 'url': self.construct_lorry_url(trove_info, remote_path),
+ 'refspecs': [
+ "+refs/heads/*",
+ "+refs/tags/*",
+ ],
+ }
+ }
+
+ def construct_lorry_url(self, trove_info, remote_path):
+ vars = dict(trove_info)
+ vars['remote_path'] = remote_path
+
+ patterns = {
+ 'ssh': 'ssh://git@{trovehost}/{remote_path}',
+ 'https':
+ 'https://{username}:{password}@{trovehost}/git/{remote_path}',
+ 'http': 'http://{trovehost}/git/{remote_path}',
+ }
+
+ return patterns[trove_info['protocol']].format(**vars)
+
+
+class ForceLsTrove(lorrycontroller.LorryControllerRoute):
+
+ http_method = 'POST'
+ path = '/1.0/force-ls-trove'
+
+ def run(self, **kwargs):
+ logging.info('%s %s called', self.http_method, self.path)
+
+ trovehost = bottle.request.forms.trovehost
+
+ statedb = self.open_statedb()
+ lister = TroveRepositoryLister(self.app_settings, self)
+ trove_info = statedb.get_trove_info(trovehost)
+ try:
+ updated = lister.list_trove_into_statedb(statedb, trove_info)
+ except GitanoLsError as e:
+ raise bottle.abort(500, str(e))
+
+ return { 'updated-troves': updated }
+
+
+class LsTroves(lorrycontroller.LorryControllerRoute):
+
+ http_method = 'POST'
+ path = '/1.0/ls-troves'
+
+ def run(self, **kwargs):
+ logging.info('%s %s called', self.http_method, self.path)
+
+ statedb = self.open_statedb()
+ lister = TroveRepositoryLister(self.app_settings, self)
+
+ trove_infos = self.get_due_troves(statedb)
+ for trove_info in trove_infos:
+ logging.info('Trove %r is due an ls', trove_info['trovehost'])
+ try:
+ lister.list_trove_into_statedb(statedb, trove_info)
+ except GitanoLsError as e:
+ bottle.abort(500, str(e))
+
+ return {
+ 'updated-troves': [trove_info['trovehost'] for trove_info in trove_infos],
+ }
+
+ def get_due_troves(self, statedb):
+ trove_infos = [
+ statedb.get_trove_info(trovehost)
+ for trovehost in statedb.get_troves()]
+ now = statedb.get_current_time()
+ return [
+ trove_info
+ for trove_info in trove_infos
+ if self.is_due(trove_info, now)]
+
+ def is_due(self, trove_info, now):
+ ls_due = trove_info['ls_last_run'] + trove_info['ls_interval']
+ return ls_due <= now