Add zuul-cloner shim

Creates a hard link copy of the source repo directory in the
destination directory. Ignores most of the zuul-cloner options
except for -m/--map, --workspace, and the list of projects.

Change-Id: I7500e23d03d1b0eef4b336f487938b3648461a27
diff --git a/tools/zuul-cloner-shim.py b/tools/zuul-cloner-shim.py
new file mode 100755
index 0000000..3d1b2ae
--- /dev/null
+++ b/tools/zuul-cloner-shim.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# Copyright 2017 Red Hat
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import os
+import re
+import sys
+import yaml
+
+from collections import defaultdict
+from collections import OrderedDict
+
+REPO_SRC_DIR = "~zuul/src/git.openstack.org/"
+
+
+# Class copied from zuul/lib/conemapper.py with minor logging changes
+class CloneMapper(object):
+
+    def __init__(self, clonemap, projects):
+        self.clonemap = clonemap
+        self.projects = projects
+
+    def expand(self, workspace):
+        print("Workspace path set to: %s" % workspace)
+
+        is_valid = True
+        ret = OrderedDict()
+        errors = []
+        for project in self.projects:
+            dests = []
+            for mapping in self.clonemap:
+                if re.match(r'^%s$' % mapping['name'], project):
+                    # Might be matched more than one time
+                    dests.append(
+                        re.sub(mapping['name'], mapping['dest'], project))
+
+            if len(dests) > 1:
+                errors.append(
+                    "Duplicate destinations for %s: %s." % (project, dests))
+                is_valid = False
+            elif len(dests) == 0:
+                print("Using %s as destination (unmatched)" % project)
+                ret[project] = [project]
+            else:
+                ret[project] = dests
+
+        if not is_valid:
+            raise Exception("Expansion errors: %s" % errors)
+
+        print("Mapping projects to workspace...")
+        for project, dest in ret.items():
+            dest = os.path.normpath(os.path.join(workspace, dest[0]))
+            ret[project] = dest
+            print("  %s -> %s" % (project, dest))
+
+        print("Checking overlap in destination directories...")
+        check = defaultdict(list)
+        for project, dest in ret.items():
+            check[dest].append(project)
+
+        dupes = dict((d, p) for (d, p) in check.items() if len(p) > 1)
+        if dupes:
+            raise Exception("Some projects share the same destination: %s",
+                            dupes)
+
+        print("Expansion completed.")
+        return ret
+
+
+def parseArgs():
+    ZUUL_ENV_SUFFIXES = ('branch', 'ref', 'url', 'project', 'newrev')
+
+    parser = argparse.ArgumentParser()
+
+    # Ignored arguments
+    parser.add_argument('-v', '--verbose', dest='verbose',
+                        action='store_true', help='IGNORED')
+    parser.add_argument('--color', dest='color', action='store_true',
+                        help='IGNORED')
+    parser.add_argument('--cache-dir', dest='cache_dir', help='IGNORED')
+    parser.add_argument('git_base_url', help='IGNORED')
+    parser.add_argument('--branch', help='IGNORED')
+    parser.add_argument('--project-branch', nargs=1, action='append',
+                        metavar='PROJECT=BRANCH', help='IGNORED')
+    for zuul_suffix in ZUUL_ENV_SUFFIXES:
+        env_name = 'ZUUL_%s' % zuul_suffix.upper()
+        parser.add_argument(
+            '--zuul-%s' % zuul_suffix, metavar='$' + env_name,
+            help='IGNORED'
+        )
+
+    # Active arguments
+    parser.add_argument('-m', '--map', dest='clone_map_file',
+                        help='specify clone map file')
+    parser.add_argument('--workspace', dest='workspace',
+                        default=os.getcwd(),
+                        help='where to clone repositories too')
+    parser.add_argument('projects', nargs='+',
+                        help='list of Gerrit projects to clone')
+
+    return parser.parse_args()
+
+
+def readCloneMap(clone_map):
+    clone_map_file = os.path.expanduser(clone_map)
+    if not os.path.exists(clone_map_file):
+        raise Exception("Unable to read clone map file at %s." %
+                        clone_map_file)
+    clone_map_file = open(clone_map_file)
+    clone_map = yaml.safe_load(clone_map_file).get('clonemap')
+    return clone_map
+
+
+def main():
+    args = parseArgs()
+
+    clone_map = []
+    if args.clone_map_file:
+        clone_map = readCloneMap(args.clone_map_file)
+
+    mapper = CloneMapper(clone_map, args.projects)
+    dests = mapper.expand(workspace=args.workspace)
+
+    for project in args.projects:
+        src = os.path.join(os.path.expanduser(REPO_SRC_DIR), project)
+        dst = dests[project]
+
+        # Remove the tail end of the path (since the copy operation will
+        # automatically create that)
+        d = dst.rstrip('/')
+        d, base = os.path.split(d)
+        if not os.path.exists(d):
+            print("Creating %s" % d)
+            os.makedirs(d)
+
+        # Create hard link copy of the source directory
+        cmd = "cp -al %s %s" % (src, dst)
+        print("%s" % cmd)
+        if os.system(cmd):
+            print("Error executing: %s" % cmd)
+            sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()