Merge branch '8567-docker-migrator' refs #8567
authorPeter Amstutz <peter.amstutz@curoverse.com>
Mon, 20 Mar 2017 18:34:17 +0000 (14:34 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Mon, 20 Mar 2017 18:34:17 +0000 (14:34 -0400)
doc/_config.yml
doc/install/migrate-docker19.html.textile.liquid [new file with mode: 0644]
docker/migrate-docker19/Dockerfile [new file with mode: 0644]
docker/migrate-docker19/build.sh [new file with mode: 0755]
docker/migrate-docker19/dnd.sh [new file with mode: 0755]
docker/migrate-docker19/migrate.sh [new file with mode: 0755]
sdk/python/arvados/commands/keepdocker.py
sdk/python/arvados/commands/migrate19.py [new file with mode: 0644]
sdk/python/bin/arv-migrate-docker19
sdk/python/tests/test_arv_keepdocker.py

index 167160c6562e7ae62705dc10acaf9fff7c4fb7f8..8c3d42a397691b3a745b77b1a9de5f4f61316c24 100644 (file)
@@ -174,3 +174,5 @@ navbar:
     - Helpful hints:
       - install/copy_pipeline_from_curoverse.html.textile.liquid
       - install/cheat_sheet.html.textile.liquid
+    - Migrating from Docker 1.9:
+      - install/migrate-docker19.html.textile.liquid
diff --git a/doc/install/migrate-docker19.html.textile.liquid b/doc/install/migrate-docker19.html.textile.liquid
new file mode 100644 (file)
index 0000000..2dca039
--- /dev/null
@@ -0,0 +1,31 @@
+---
+layout: default
+navsection: installguide
+title: Migrating Docker images
+...
+
+If you have an existing Arvados installation using Docker 1.9 and wish to update to Docker 1.10+, you must migrate the Docker images stored in Keep.
+
+The @arv-migrate-docker19@ tool converts Docker images stored in Arvados from image format v1 (Docker <= 1.9) to image format v2 (Docker >= 1.10).
+
+Requires Docker running on the local host (can be either 1.9 or 1.10+).
+
+Usage:
+
+# Run @arvados/docker/migrate-docker19/build.sh@ to create @arvados/migrate-docker19@ Docker image.
+# Set ARVADOS_API_HOST and ARVADOS_API_TOKEN to the cluster you want to migrate.
+# Run @arv-migrate-docker19@ from the Arvados Python SDK on the host (not in a container).
+
+This will query Arvados for v1 format Docker images.  For each image that does not already have a corresponding v2 format image (as indicated by a docker_image_migration tag) it will perform the following process:
+
+i) download the image from Arvados
+ii) load it into Docker
+iii) update the Docker version, which updates the image
+iv) save the v2 format image and upload to Arvados
+v) create a migration link
+
+Once the Docker images in Keep have been migrated, upgrade the version of Docker used across the cluster.  Finally, update the API server configuration from "v1" to "v2" to reflect the supported Docker image version:
+
+<pre>
+docker_image_formats: ["v2"]
+</pre>
diff --git a/docker/migrate-docker19/Dockerfile b/docker/migrate-docker19/Dockerfile
new file mode 100644 (file)
index 0000000..7e921c8
--- /dev/null
@@ -0,0 +1,31 @@
+FROM debian:8
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-key adv --keyserver pool.sks-keyservers.net --recv 1078ECD7 && \
+    gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    apt-key adv --keyserver hkp://pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D || \
+    apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
+
+VOLUME /var/lib/docker
+
+RUN mkdir -p /etc/apt/sources.list.d && \
+    echo deb http://apt.arvados.org/ jessie main > /etc/apt/sources.list.d/apt.arvados.org.list && \
+    apt-get clean && \
+    apt-get update && \
+    apt-get install -yq --no-install-recommends -o Acquire::Retries=6 \
+        git curl python-arvados-python-client apt-transport-https ca-certificates && \
+    apt-get clean
+
+RUN echo deb https://apt.dockerproject.org/repo debian-jessie main > /etc/apt/sources.list.d/docker.list && \
+    apt-get update && \
+    apt-get install -yq --no-install-recommends -o Acquire::Retries=6 \
+        docker-engine=1.9.1-0~jessie && \
+    apt-get clean
+
+RUN mkdir /root/pkgs && \
+    cd /root/pkgs && \
+    curl -L -O https://apt.dockerproject.org/repo/pool/main/d/docker-engine/docker-engine_1.13.1-0~debian-jessie_amd64.deb && \
+    curl -L -O http://httpredir.debian.org/debian/pool/main/libt/libtool/libltdl7_2.4.2-1.11+b1_amd64.deb
+
+ADD migrate.sh dnd.sh /root/
diff --git a/docker/migrate-docker19/build.sh b/docker/migrate-docker19/build.sh
new file mode 100755 (executable)
index 0000000..3a36cd4
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec docker build -t arvados/migrate-docker19 .
diff --git a/docker/migrate-docker19/dnd.sh b/docker/migrate-docker19/dnd.sh
new file mode 100755 (executable)
index 0000000..ce72601
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# Taken from https://github.com/jpetazzo/dind
+
+exec 2>&1
+
+# Ensure that all nodes in /dev/mapper correspond to mapped devices currently loaded by the device-mapper kernel driver
+dmsetup mknodes
+
+: {LOG:=stdio}
+
+# First, make sure that cgroups are mounted correctly.
+CGROUP=/sys/fs/cgroup
+[ -d $CGROUP ] || mkdir $CGROUP
+
+if mountpoint -q $CGROUP ; then
+    break
+else
+    mount -n -t tmpfs -o uid=0,gid=0,mode=0755 cgroup $CGROUP
+fi
+
+if ! mountpoint -q $CGROUP ; then
+    echo "Could not find or mount cgroups. Tried /sys/fs/cgroup and /cgroup.  Did you use --privileged?"
+    exit 1
+fi
+
+if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security
+then
+    mount -t securityfs none /sys/kernel/security || {
+        echo "Could not mount /sys/kernel/security."
+        echo "AppArmor detection and --privileged mode might break."
+    }
+fi
+
+# Mount the cgroup hierarchies exactly as they are in the parent system.
+for SUBSYS in $(cut -d: -f2 /proc/1/cgroup)
+do
+        [ -d $CGROUP/$SUBSYS ] || mkdir $CGROUP/$SUBSYS
+        mountpoint -q $CGROUP/$SUBSYS ||
+                mount -n -t cgroup -o $SUBSYS cgroup $CGROUP/$SUBSYS
+
+        # The two following sections address a bug which manifests itself
+        # by a cryptic "lxc-start: no ns_cgroup option specified" when
+        # trying to start containers withina container.
+        # The bug seems to appear when the cgroup hierarchies are not
+        # mounted on the exact same directories in the host, and in the
+        # container.
+
+        # Named, control-less cgroups are mounted with "-o name=foo"
+        # (and appear as such under /proc/<pid>/cgroup) but are usually
+        # mounted on a directory named "foo" (without the "name=" prefix).
+        # Systemd and OpenRC (and possibly others) both create such a
+        # cgroup. To avoid the aforementioned bug, we symlink "foo" to
+        # "name=foo". This shouldn't have any adverse effect.
+        echo $SUBSYS | grep -q ^name= && {
+                NAME=$(echo $SUBSYS | sed s/^name=//)
+                ln -s $SUBSYS $CGROUP/$NAME
+        }
+
+        # Likewise, on at least one system, it has been reported that
+        # systemd would mount the CPU and CPU accounting controllers
+        # (respectively "cpu" and "cpuacct") with "-o cpuacct,cpu"
+        # but on a directory called "cpu,cpuacct" (note the inversion
+        # in the order of the groups). This tries to work around it.
+        [ $SUBSYS = cpuacct,cpu ] && ln -s $SUBSYS $CGROUP/cpu,cpuacct
+done
+
+# Note: as I write those lines, the LXC userland tools cannot setup
+# a "sub-container" properly if the "devices" cgroup is not in its
+# own hierarchy. Let's detect this and issue a warning.
+grep -q :devices: /proc/1/cgroup ||
+       echo "WARNING: the 'devices' cgroup should be in its own hierarchy."
+grep -qw devices /proc/1/cgroup ||
+       echo "WARNING: it looks like the 'devices' cgroup is not mounted."
+
+# Now, close extraneous file descriptors.
+pushd /proc/self/fd >/dev/null
+for FD in *
+do
+       case "$FD" in
+       # Keep stdin/stdout/stderr
+       [012])
+               ;;
+       # Nuke everything else
+       *)
+               eval exec "$FD>&-"
+               ;;
+       esac
+done
+popd >/dev/null
+
+
+# If a pidfile is still around (for example after a container restart),
+# delete it so that docker can start.
+rm -rf /var/run/docker.pid
+
+read pid cmd state ppid pgrp session tty_nr tpgid rest < /proc/self/stat
+
+if ! docker daemon --storage-driver=overlay $DOCKER_DAEMON_ARGS ; then
+    docker daemon $DOCKER_DAEMON_ARGS
+fi
diff --git a/docker/migrate-docker19/migrate.sh b/docker/migrate-docker19/migrate.sh
new file mode 100755 (executable)
index 0000000..58d6665
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+set -e
+
+function cleanup {
+    kill $(cat /var/run/docker.pid)
+    sleep 1
+    rm -rf /var/lib/docker/*
+}
+
+trap cleanup EXIT
+
+/root/dnd.sh &
+sleep 2
+
+image_tar_keepref=$1
+image_id=$2
+image_repo=$3
+image_tag=$4
+project_uuid=$5
+
+arv-get $image_tar_keepref | docker load
+
+docker tag $image_id $image_repo:$image_tag
+
+docker images -a
+
+kill $(cat /var/run/docker.pid)
+sleep 1
+
+cd /root/pkgs
+dpkg -i libltdl7_2.4.2-1.11+b1_amd64.deb  docker-engine_1.13.1-0~debian-jessie_amd64.deb
+
+/root/dnd.sh &
+sleep 2
+
+docker images -a
+
+UUID=$(arv-keepdocker --force-image-format --project-uuid=$project_uuid $image_repo $image_tag)
+
+echo "Migrated uuid is $UUID"
index 3ffb7f36b6b8f7962c711cc19e1f59460346a6ff..a0dc43ea366f833cb22295dffad985d60b1f521b 100644 (file)
@@ -11,6 +11,7 @@ import subprocess
 import sys
 import tarfile
 import tempfile
+import shutil
 import _strptime
 
 from operator import itemgetter
@@ -20,10 +21,17 @@ import arvados
 import arvados.util
 import arvados.commands._util as arv_cmd
 import arvados.commands.put as arv_put
+from arvados.collection import CollectionReader
 import ciso8601
+import logging
+import arvados.config
 
 from arvados._version import __version__
 
+logger = logging.getLogger('arvados.keepdocker')
+logger.setLevel(logging.DEBUG if arvados.config.get('ARVADOS_DEBUG')
+                else logging.INFO)
+
 EARLIEST_DATETIME = datetime.datetime(datetime.MINYEAR, 1, 1, 0, 0, 0)
 STAT_CACHE_ERRORS = (IOError, OSError, ValueError)
 
@@ -103,15 +111,15 @@ def docker_image_format(image_hash):
 def docker_image_compatible(api, image_hash):
     supported = api._rootDesc.get('dockerImageFormats', [])
     if not supported:
-        print >>sys.stderr, "arv-keepdocker: warning: server does not specify supported image formats (see docker_image_formats in server config). Continuing."
+        logger.warn("server does not specify supported image formats (see docker_image_formats in server config). Continuing.")
         return True
 
     fmt = docker_image_format(image_hash)
     if fmt in supported:
         return True
     else:
-        print >>sys.stderr, "arv-keepdocker: image format is {!r} " \
-            "but server supports only {!r}".format(fmt, supported)
+        logger.error("image format is {!r} " \
+            "but server supports only {!r}".format(fmt, supported))
         return False
 
 def docker_images():
@@ -330,62 +338,6 @@ def _uuid2pdh(api, uuid):
         select=['portable_data_hash'],
     ).execute()['items'][0]['portable_data_hash']
 
-_migration_link_class = 'docker_image_migration'
-_migration_link_name = 'migrate_1.9_1.10'
-def _migrate19_link(api, root_uuid, old_uuid, new_uuid):
-    old_pdh = _uuid2pdh(api, old_uuid)
-    new_pdh = _uuid2pdh(api, new_uuid)
-    if not api.links().list(filters=[
-            ['owner_uuid', '=', root_uuid],
-            ['link_class', '=', _migration_link_class],
-            ['name', '=', _migration_link_name],
-            ['tail_uuid', '=', old_pdh],
-            ['head_uuid', '=', new_pdh]]).execute()['items']:
-        print >>sys.stderr, 'Creating migration link {} -> {}: '.format(
-            old_pdh, new_pdh),
-        link = api.links().create(body={
-            'owner_uuid': root_uuid,
-            'link_class': _migration_link_class,
-            'name': _migration_link_name,
-            'tail_uuid': old_pdh,
-            'head_uuid': new_pdh,
-        }).execute()
-        print >>sys.stderr, '{}'.format(link['uuid'])
-        return link
-
-def migrate19():
-    api = arvados.api('v1')
-    user = api.users().current().execute()
-    if not user['is_admin']:
-        raise Exception("This command requires an admin token")
-    root_uuid = user['uuid'][:12] + '000000000000000'
-    new_image_uuids = {}
-    images = list_images_in_arv(api, 2)
-    is_new = lambda img: img['dockerhash'].startswith('sha256:')
-
-    count_new = 0
-    for uuid, img in images:
-        if not re.match(r'^[0-9a-f]{64}$', img["tag"]):
-            continue
-        key = (img["repo"], img["tag"])
-        if is_new(img) and key not in new_image_uuids:
-            count_new += 1
-            new_image_uuids[key] = uuid
-
-    count_migrations = 0
-    new_links = []
-    for uuid, img in images:
-        key = (img['repo'], img['tag'])
-        if not is_new(img) and key in new_image_uuids:
-            count_migrations += 1
-            link = _migrate19_link(api, root_uuid, uuid, new_image_uuids[key])
-            if link:
-                new_links.append(link)
-
-    print >>sys.stderr, "=== {} new-format images, {} migrations detected, " \
-        "{} links added.".format(count_new, count_migrations, len(new_links))
-    return new_links
-
 def main(arguments=None, stdout=sys.stdout):
     args = arg_parser.parse_args(arguments)
     api = arvados.api('v1')
@@ -393,8 +345,14 @@ def main(arguments=None, stdout=sys.stdout):
     if args.image is None or args.image == 'images':
         fmt = "{:30}  {:10}  {:12}  {:29}  {:20}\n"
         stdout.write(fmt.format("REPOSITORY", "TAG", "IMAGE ID", "COLLECTION", "CREATED"))
-        for i, j in list_images_in_arv(api, args.retries):
-            stdout.write(fmt.format(j["repo"], j["tag"], j["dockerhash"][0:12], i, j["timestamp"].strftime("%c")))
+        try:
+            for i, j in list_images_in_arv(api, args.retries):
+                stdout.write(fmt.format(j["repo"], j["tag"], j["dockerhash"][0:12], i, j["timestamp"].strftime("%c")))
+        except IOError as e:
+            if e.errno == errno.EPIPE:
+                pass
+            else:
+                raise
         sys.exit(0)
 
     # Pull the image if requested, unless the image is specified as a hash
@@ -405,15 +363,15 @@ def main(arguments=None, stdout=sys.stdout):
     try:
         image_hash = find_one_image_hash(args.image, args.tag)
     except DockerError as error:
-        print >>sys.stderr, "arv-keepdocker:", error.message
+        logger.error(error.message)
         sys.exit(1)
 
     if not docker_image_compatible(api, image_hash):
         if args.force_image_format:
-            print >>sys.stderr, "arv-keepdocker: forcing incompatible image"
+            logger.warn("forcing incompatible image")
         else:
-            print >>sys.stderr, "arv-keepdocker: refusing to store " \
-                "incompatible format (use --force-image-format to override)"
+            logger.error("refusing to store " \
+                "incompatible format (use --force-image-format to override)")
             sys.exit(1)
 
     image_repo_tag = '{}:{}'.format(args.image, args.tag) if not image_hash.startswith(args.image.lower()) else None
@@ -455,7 +413,7 @@ def main(arguments=None, stdout=sys.stdout):
                         api, args.retries,
                         filters=[['link_class', '=', 'docker_image_repo+tag'],
                                  ['name', '=', image_repo_tag],
-                                 ['head_uuid', 'in', collections]])
+                                 ['head_uuid', 'in', [c["uuid"] for c in collections]]])
                 else:
                     existing_repo_tag = []
 
diff --git a/sdk/python/arvados/commands/migrate19.py b/sdk/python/arvados/commands/migrate19.py
new file mode 100644 (file)
index 0000000..e75095b
--- /dev/null
@@ -0,0 +1,204 @@
+import argparse
+import time
+import sys
+import logging
+import shutil
+import tempfile
+import os
+import subprocess
+import re
+
+import arvados
+import arvados.commands.keepdocker
+from arvados._version import __version__
+from arvados.collection import CollectionReader
+
+logger = logging.getLogger('arvados.migrate-docker19')
+logger.setLevel(logging.DEBUG if arvados.config.get('ARVADOS_DEBUG')
+                else logging.INFO)
+
+_migration_link_class = 'docker_image_migration'
+_migration_link_name = 'migrate_1.9_1.10'
+
+class MigrationFailed(Exception):
+    pass
+
+def main(arguments=None):
+    """Docker image format migration tool for Arvados.
+
+    This converts Docker images stored in Arvados from image format v1
+    (Docker <= 1.9) to image format v2 (Docker >= 1.10).
+
+    Requires Docker running on the local host.
+
+    Usage:
+
+    1) Run arvados/docker/migrate-docker19/build.sh to create
+    arvados/migrate-docker19 Docker image.
+
+    2) Set ARVADOS_API_HOST and ARVADOS_API_TOKEN to the cluster you want to migrate.
+
+    3) Run arv-migrate-docker19 from the Arvados Python SDK on the host (not in a container).
+
+    This will query Arvados for v1 format Docker images.  For each image that
+    does not already have a corresponding v2 format image (as indicated by a
+    docker_image_migration tag) it will perform the following process:
+
+    i) download the image from Arvados
+    ii) load it into Docker
+    iii) update the Docker version, which updates the image
+    iv) save the v2 format image and upload to Arvados
+    v) create a migration link
+
+    """
+
+    migrate19_parser = argparse.ArgumentParser()
+    migrate19_parser.add_argument(
+        '--version', action='version', version="%s %s" % (sys.argv[0], __version__),
+        help='Print version and exit.')
+
+    exgroup = migrate19_parser.add_mutually_exclusive_group()
+    exgroup.add_argument(
+        '--dry-run', action='store_true', help="Print number of pending migrations.")
+    exgroup.add_argument(
+        '--print-unmigrated', action='store_true',
+        default=False, help="Print list of images needing migration.")
+
+    migrate19_parser.add_argument('infile', nargs='?', type=argparse.FileType('r'),
+                                  default=None, help="List of images to be migrated")
+
+    args = migrate19_parser.parse_args(arguments)
+
+    only_migrate = None
+    if args.infile:
+        only_migrate = set()
+        for l in args.infile:
+            only_migrate.add(l.strip())
+
+    api_client  = arvados.api()
+
+    user = api_client.users().current().execute()
+    if not user['is_admin']:
+        raise Exception("This command requires an admin token")
+    sys_uuid = user['uuid'][:12] + '000000000000000'
+
+    images = arvados.commands.keepdocker.list_images_in_arv(api_client, 3)
+
+    is_new = lambda img: img['dockerhash'].startswith('sha256:')
+
+    count_new = 0
+    old_images = []
+    for uuid, img in images:
+        if img["dockerhash"].startswith("sha256:"):
+            continue
+        key = (img["repo"], img["tag"], img["timestamp"])
+        old_images.append(img)
+
+    migration_links = arvados.util.list_all(api_client.links().list, filters=[
+        ['link_class', '=', _migration_link_class],
+        ['name', '=', _migration_link_name],
+    ])
+
+    already_migrated = set()
+    for m in migration_links:
+        already_migrated.add(m["tail_uuid"])
+
+    items = arvados.util.list_all(api_client.collections().list,
+                                  filters=[["uuid", "in", [img["collection"] for img in old_images]]],
+                                  select=["uuid", "portable_data_hash"])
+    uuid_to_pdh = {i["uuid"]: i["portable_data_hash"] for i in items}
+
+    need_migrate = {}
+    for img in old_images:
+        pdh = uuid_to_pdh[img["collection"]]
+        if pdh not in already_migrated and (only_migrate is None or pdh in only_migrate):
+            need_migrate[pdh] = img
+
+    if args.print_unmigrated:
+        only_migrate = set()
+        for pdh in need_migrate:
+            print pdh
+        return
+
+    logger.info("Already migrated %i images", len(already_migrated))
+    logger.info("Need to migrate %i images", len(need_migrate))
+
+    if args.dry_run:
+        return
+
+    success = []
+    failures = []
+    count = 1
+    for old_image in need_migrate.values():
+        if uuid_to_pdh[old_image["collection"]] in already_migrated:
+            continue
+
+        logger.info("[%i/%i] Migrating %s:%s (%s)", count, len(need_migrate), old_image["repo"], old_image["tag"], old_image["collection"])
+        count += 1
+        start = time.time()
+
+        oldcol = CollectionReader(old_image["collection"])
+        tarfile = oldcol.keys()[0]
+
+        varlibdocker = tempfile.mkdtemp()
+        try:
+            with tempfile.NamedTemporaryFile() as envfile:
+                envfile.write("ARVADOS_API_HOST=%s\n" % (os.environ["ARVADOS_API_HOST"]))
+                envfile.write("ARVADOS_API_TOKEN=%s\n" % (os.environ["ARVADOS_API_TOKEN"]))
+                if "ARVADOS_API_HOST_INSECURE" in os.environ:
+                    envfile.write("ARVADOS_API_HOST_INSECURE=%s\n" % (os.environ["ARVADOS_API_HOST_INSECURE"]))
+                envfile.flush()
+
+                dockercmd = ["docker", "run",
+                             "--privileged",
+                             "--rm",
+                             "--env-file", envfile.name,
+                             "--volume", "%s:/var/lib/docker" % varlibdocker,
+                             "arvados/migrate-docker19",
+                             "/root/migrate.sh",
+                             "%s/%s" % (old_image["collection"], tarfile),
+                             tarfile[0:40],
+                             old_image["repo"],
+                             old_image["tag"],
+                             oldcol.api_response()["owner_uuid"]]
+
+                proc = subprocess.Popen(dockercmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                out, err = proc.communicate()
+
+                if proc.returncode != 0:
+                    logger.error("Failed with return code %i", proc.returncode)
+                    logger.error("--- Stdout ---\n%s", out)
+                    logger.error("--- Stderr ---\n%s", err)
+                    raise MigrationFailed()
+
+            migrated = re.search(r"Migrated uuid is ([a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15})", out)
+            if migrated:
+                newcol = CollectionReader(migrated.group(1))
+
+                api_client.links().create(body={"link": {
+                    'owner_uuid': sys_uuid,
+                    'link_class': _migration_link_class,
+                    'name': _migration_link_name,
+                    'tail_uuid': oldcol.portable_data_hash(),
+                    'head_uuid': newcol.portable_data_hash()
+                    }}).execute(num_retries=3)
+
+                logger.info("Migrated '%s' (%s) to '%s' (%s) in %is",
+                            oldcol.portable_data_hash(), old_image["collection"],
+                            newcol.portable_data_hash(), migrated.group(1),
+                            time.time() - start)
+                already_migrated.add(oldcol.portable_data_hash())
+                success.append(old_image["collection"])
+            else:
+                logger.error("Error migrating '%s'", old_image["collection"])
+                failures.append(old_image["collection"])
+        except Exception as e:
+            logger.error("Failed to migrate %s in %is", old_image["collection"], time.time() - start,
+                         exc_info=(not isinstance(e, MigrationFailed)))
+            failures.append(old_image["collection"])
+        finally:
+            shutil.rmtree(varlibdocker)
+
+    logger.info("Successfully migrated %i images", len(success))
+    if failures:
+        logger.error("Failed to migrate %i images", len(failures))
index fcef8d744d813be3c9b5b285635d01b1bf97ff1f..8d928a1c69ab41f350a2dfab1a8c34c2c83cf880 100755 (executable)
@@ -1,4 +1,4 @@
 #!/usr/bin/env python
 
-from arvados.commands.keepdocker import migrate19
-migrate19()
+from arvados.commands.migrate19 import main
+main()
index 9fcf2f1bab80513fb57e38eeed4703793d03a35f..e67aacc87b1bbd27f10f885878462bd7ae235d1f 100644 (file)
@@ -10,6 +10,7 @@ import subprocess
 import sys
 import tempfile
 import unittest
+import logging
 
 import arvados.commands.keepdocker as arv_keepdocker
 import arvados_testutil as tutil
@@ -21,39 +22,28 @@ class StopTest(Exception):
 
 
 class ArvKeepdockerTestCase(unittest.TestCase):
-    def run_arv_keepdocker(self, args):
+    def run_arv_keepdocker(self, args, err):
         sys.argv = ['arv-keepdocker'] + args
-        return arv_keepdocker.main()
+        log_handler = logging.StreamHandler(err)
+        arv_keepdocker.logger.addHandler(log_handler)
+        try:
+            return arv_keepdocker.main()
+        finally:
+            arv_keepdocker.logger.removeHandler(log_handler)
 
     def test_unsupported_arg(self):
         with self.assertRaises(SystemExit):
-            self.run_arv_keepdocker(['-x=unknown'])
+            self.run_arv_keepdocker(['-x=unknown'], sys.stderr)
 
     def test_version_argument(self):
         err = io.BytesIO()
         out = io.BytesIO()
         with tutil.redirected_streams(stdout=out, stderr=err):
             with self.assertRaises(SystemExit):
-                self.run_arv_keepdocker(['--version'])
+                self.run_arv_keepdocker(['--version'], sys.stderr)
         self.assertEqual(out.getvalue(), '')
         self.assertRegexpMatches(err.getvalue(), "[0-9]+\.[0-9]+\.[0-9]+")
 
-    def test_migrate19(self):
-        try:
-            sys.argv = ['arv-migrate-docker19']
-
-            added = arv_keepdocker.migrate19()
-            self.assertEqual(len(added), 1)
-            self.assertEqual(added[0]['link_class'], 'docker_image_migration')
-            self.assertEqual(added[0]['name'], 'migrate_1.9_1.10')
-            self.assertEqual(added[0]['tail_uuid'], 'fa3c1a9cb6783f85f2ecda037e07b8c3+167')
-            self.assertEqual(added[0]['head_uuid'], 'd740a57097711e08eb9b2a93518f20ab+174')
-
-            added = arv_keepdocker.migrate19()
-            self.assertEqual(added, [])
-        finally:
-            run_test_server.reset()
-
     @mock.patch('arvados.commands.keepdocker.find_image_hashes',
                 return_value=['abc123'])
     @mock.patch('arvados.commands.keepdocker.find_one_image_hash',
@@ -80,7 +70,7 @@ class ArvKeepdockerTestCase(unittest.TestCase):
             err = io.BytesIO()
             out = io.BytesIO()
 
-            with tutil.redirected_streams(stdout=out, stderr=err), \
+            with tutil.redirected_streams(stdout=out), \
                  mock.patch('arvados.api') as api, \
                  mock.patch('arvados.commands.keepdocker.popen_docker',
                             return_value=subprocess.Popen(
@@ -91,7 +81,7 @@ class ArvKeepdockerTestCase(unittest.TestCase):
                  self.assertRaises(StopTest if expect_ok else SystemExit):
 
                 api()._rootDesc = fakeDD
-                self.run_arv_keepdocker(['--force', 'testimage'])
+                self.run_arv_keepdocker(['--force', 'testimage'], err)
 
             self.assertEqual(out.getvalue(), '')
             if expect_ok:
@@ -112,7 +102,7 @@ class ArvKeepdockerTestCase(unittest.TestCase):
         fakeDD['dockerImageFormats'] = ['v1']
         err = io.BytesIO()
         out = io.BytesIO()
-        with tutil.redirected_streams(stdout=out, stderr=err), \
+        with tutil.redirected_streams(stdout=out), \
              mock.patch('arvados.api') as api, \
              mock.patch('arvados.commands.keepdocker.popen_docker',
                         return_value=subprocess.Popen(
@@ -123,5 +113,5 @@ class ArvKeepdockerTestCase(unittest.TestCase):
              self.assertRaises(StopTest):
             api()._rootDesc = fakeDD
             self.run_arv_keepdocker(
-                ['--force', '--force-image-format', 'testimage'])
+                ['--force', '--force-image-format', 'testimage'], err)
         self.assertRegexpMatches(err.getvalue(), "forcing incompatible image")