diff --git a/Tools/ART/python/ART/art_base.py b/Tools/ART/python/ART/art_base.py
index c9032de7cb6e744b42b8688111c4024d781d69c2..6a683081840fcbfb25eaf13f37ff3ad8f5488451 100755
--- a/Tools/ART/python/ART/art_base.py
+++ b/Tools/ART/python/ART/art_base.py
@@ -6,13 +6,20 @@ __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
 
 import fnmatch
 import inspect
+import logging
 import os
-import sys
 import yaml
 
+try:
+    import scandir as scan
+except ImportError:
+    import os as scan
+
 from art_misc import is_exe, run_command
 from art_header import ArtHeader
 
+MODULE = "art.base"
+
 
 class ArtBase(object):
     """TBD."""
@@ -51,26 +58,37 @@ class ArtBase(object):
 
     def validate(self, script_directory):
         """TBD."""
+        log = logging.getLogger(MODULE)
         directories = self.get_test_directories(script_directory.rstrip("/"))
+
+        found_test = False
         for directory in directories.itervalues():
             files = self.get_files(directory)
             for fname in files:
                 test_name = os.path.join(directory, fname)
-                print test_name
+                found_test = True
+                log.debug(test_name)
                 if not is_exe(test_name):
-                    print "ERROR: ", test_name, "is not executable."
+                    log.error("%s is not executable.", test_name)
                 ArtHeader(test_name).validate()
+
+        if not found_test:
+            log.warning('No scripts found in %s directory', directories.values()[0])
+            return 0
+
+        log.info("Scripts in %s directory are validated", script_directory)
         return 0
 
     def included(self, script_directory, job_type, index_type, nightly_release, project, platform):
         """TBD."""
+        log = logging.getLogger(MODULE)
         directories = self.get_test_directories(script_directory.rstrip("/"))
         for directory in directories.itervalues():
             files = self.get_files(directory, job_type, index_type)
             for fname in files:
                 test_name = os.path.join(directory, fname)
                 if self.is_included(test_name, nightly_release, project, platform):
-                    print test_name, ArtHeader(test_name).get('art-include')
+                    log.info("%s %s", test_name, ArtHeader(test_name).get('art-include'))
         return 0
 
     def download(self, input_file):
@@ -84,6 +102,8 @@ class ArtBase(object):
         """TBD."""
         import PyUtils.PoolFile as PF
 
+        log = logging.getLogger(MODULE)
+
         # diff-pool
         df = PF.DiffFiles(refFileName=ref_file, chkFileName=file_name, ignoreList=['RecoTimingObj_p1_RAWtoESD_timings', 'RecoTimingObj_p1_ESDtoAOD_timings'])
         df.printSummary()
@@ -94,22 +114,24 @@ class ArtBase(object):
         # diff-root
         (code, out, err) = run_command("acmd.py diff-root " + file_name + " " + ref_file + " --error-mode resilient --ignore-leaves RecoTimingObj_p1_HITStoRDO_timings RecoTimingObj_p1_RAWtoESD_mems RecoTimingObj_p1_RAWtoESD_timings RAWtoESD_mems RAWtoESD_timings ESDtoAOD_mems ESDtoAOD_timings HITStoRDO_timings RAWtoALL_mems RAWtoALL_timings RecoTimingObj_p1_RAWtoALL_mems RecoTimingObj_p1_RAWtoALL_timings RecoTimingObj_p1_EVNTtoHITS_timings --entries " + str(entries))
         if code != 0:
-            print "Error:", code
-            print "StdErr:", err
+            log.error("Error: %d", code)
+            print(err)
 
-        print out
-        sys.stdout.flush()
-        return err
+        log.info(out)
+        return code
 
     #
     # Protected Methods
     #
     def get_config(self):
-        """Retrieve dictionary of ART configuration file."""
-        config_file = open("art-configuration.yml", "r")
-        config = yaml.load(config_file)
-        config_file.close()
-        return config
+        """Retrieve dictionary of ART configuration file, or None if file does not exist."""
+        try:
+            config_file = open("art-configuration.yml", "r")
+            config = yaml.load(config_file)
+            config_file.close()
+            return config
+        except IOError:
+            return None
 
     def get_files(self, directory, job_type=None, index_type="all", nightly_release=None, project=None, platform=None):
         """
@@ -163,7 +185,7 @@ class ArtBase(object):
         A dictionary key=<package>, value=<directory> is returned
         """
         result = {}
-        for root, dirs, files in os.walk(directory):
+        for root, dirs, files in scan.walk(directory):
             if root.endswith('/test'):
                 package = os.path.basename(os.path.dirname(root))
                 result[package] = root
diff --git a/Tools/ART/python/ART/art_build.py b/Tools/ART/python/ART/art_build.py
index 3c62cb3b11b5396ea66bea42f6b105dd9f4c13e0..0c58f19886db14f6bbbb07d5858a0dbeb8bd3977 100644
--- a/Tools/ART/python/ART/art_build.py
+++ b/Tools/ART/python/ART/art_build.py
@@ -7,6 +7,7 @@ __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
 import collections
 import fnmatch
 import json
+import logging
 import multiprocessing
 import os
 import re
@@ -17,12 +18,16 @@ from art_header import ArtHeader
 
 from parallelScheduler import ParallelScheduler
 
+MODULE = "art.build"
+
 
 def run_job(art_directory, sequence_tag, script_directory, package, job_type, index, test_name, nightly_release, project, platform, nightly_tag):
     """TBD."""
-    print "job started", art_directory, sequence_tag, script_directory, package, job_type, index, test_name, nightly_release, project, platform, nightly_tag
+    log = logging.getLogger(MODULE)
+
+    log.info("job started %s %s %s %s %s %d %s %s %s %s %s", art_directory, sequence_tag, script_directory, package, job_type, index, test_name, nightly_release, project, platform, nightly_tag)
     (exit_code, out, err) = run_command(' '.join((os.path.join(art_directory, './art-internal.py'), "job", "build", script_directory, package, job_type, sequence_tag, str(index), "out", nightly_release, project, platform, nightly_tag)))
-    print "job ended", art_directory, sequence_tag, script_directory, package, job_type, index, test_name, nightly_release, project, platform, nightly_tag
+    log.info("job ended %s %s %s %s %s %d %s %s %s %s %s", art_directory, sequence_tag, script_directory, package, job_type, index, test_name, nightly_release, project, platform, nightly_tag)
 
     return (test_name, exit_code, out, err)
 
@@ -33,7 +38,8 @@ class ArtBuild(ArtBase):
     def __init__(self, art_directory, nightly_release, project, platform, nightly_tag, script_directory, max_jobs=0, ci=False):
         """TBD."""
         super(ArtBuild, self).__init__(art_directory)
-        # print "ArtBuild", art_directory, script_directory, max_jobs
+        log = logging.getLogger(MODULE)
+        log.debug("ArtBuild %s %s %d", art_directory, script_directory, max_jobs)
         self.art_directory = art_directory
         self.script_directory = script_directory.rstrip("/")
         self.nightly_release = nightly_release
@@ -45,10 +51,11 @@ class ArtBuild(ArtBase):
 
     def task_list(self, job_type, sequence_tag):
         """TBD."""
-        # print "task_list", job_type, sequence_tag
+        log = logging.getLogger(MODULE)
+        log.debug("task_list %s %s", job_type, sequence_tag)
         test_directories = self.get_test_directories(self.script_directory)
         if not test_directories:
-            print 'WARNING: No tests found in directories ending in "test"'
+            log.warning('No tests found in directories ending in "test"')
 
         status = collections.defaultdict(lambda: collections.defaultdict(lambda: collections.defaultdict()))
 
@@ -81,7 +88,8 @@ class ArtBuild(ArtBase):
 
     def task(self, package, job_type, sequence_tag):
         """TBD."""
-        # print "task", package, job_type, sequence_tag
+        log = logging.getLogger(MODULE)
+        log.debug("task %s %s %s", package, job_type, sequence_tag)
         test_names = self.get_list(self.script_directory, package, job_type, "all")
         scheduler = ParallelScheduler(self.max_jobs + 1)
 
@@ -101,7 +109,7 @@ class ArtBuild(ArtBase):
 
             if not os.access(fname, os.X_OK):
                 schedule_test = False
-                print "job skipped, file not executable: ", fname
+                log.warning("job skipped, file not executable: %s", fname)
 
             if schedule_test:
                 scheduler.add_task(task_name="t" + str(index), dependencies=[], description="d", target_function=run_job, function_kwargs={'art_directory': self.art_directory, 'sequence_tag': sequence_tag, 'script_directory': self.script_directory, 'package': package, 'job_type': job_type, 'index': index, 'test_name': test_name, 'nightly_release': self.nightly_release, 'project': self.project, 'platform': self.platform, 'nightly_tag': self.nightly_tag})
@@ -112,7 +120,8 @@ class ArtBuild(ArtBase):
 
     def job(self, package, job_type, sequence_tag, index, out):
         """TBD."""
-        # print "job", package, job_type, sequence_tag, index, out
+        log = logging.getLogger(MODULE)
+        log.debug("job %s %s %s %d %s", package, job_type, sequence_tag, index, out)
         test_directories = self.get_test_directories(self.script_directory)
         test_directory = os.path.abspath(test_directories[package])
         test_name = self.get_files(test_directory, job_type)[int(index)]
diff --git a/Tools/ART/python/ART/art_grid.py b/Tools/ART/python/ART/art_grid.py
index 35e032d525890ffacc3bfe4f2d15fbed8ff5ec16..6c74cf0e38eaa956856f46d81b4c57fccdc19ae8 100644
--- a/Tools/ART/python/ART/art_grid.py
+++ b/Tools/ART/python/ART/art_grid.py
@@ -8,6 +8,7 @@ import atexit
 import datetime
 import glob
 import json
+import logging
 import os
 import re
 import shutil
@@ -15,15 +16,38 @@ import sys
 import tarfile
 import tempfile
 
+try:
+    import rucio.client
+    RUCIO = True
+except ImportError:
+    # NOTE: defer logging as level is not set yet
+    RUCIO = False
 
 from art_base import ArtBase
 from art_header import ArtHeader
-from art_misc import mkdir_p, make_executable, run_command, exit_on_failure
+from art_misc import mkdir_p, make_executable, run_command
+
+MODULE = "art.grid"
 
 
 class ArtGrid(ArtBase):
     """TBD."""
 
+    CVMFS_DIRECTORY = '/cvmfs/atlas-nightlies.cern.ch/repo/sw'
+
+    LOG = '.log'
+    JSON = '_EXT0'
+    OUTPUT = '_EXT1'
+
+    ARTPROD = 'artprod'
+    ART_JOB = 'art-job.json'
+    LOG_TGZ = 'log.tgz'
+    JOB_TAR = 'job.tar'
+    JOB_REPORT = 'jobReport.json'
+    JOB_REPORT_ART_KEY = 'art'
+
+    ATHENA_STDOUT = 'athena_stdout.txt'
+
     def __init__(self, art_directory, nightly_release, project, platform, nightly_tag, script_directory=None, skip_setup=False, submit_directory=None):
         """TBD."""
         super(ArtGrid, self).__init__(art_directory)
@@ -34,104 +58,478 @@ class ArtGrid(ArtBase):
         self.script_directory = script_directory
         self.skip_setup = skip_setup
         self.submit_directory = submit_directory
+        self.rucio_cache = os.path.join(tempfile.gettempdir(), "rucio-cache")
 
-        self.cvmfs_directory = '/cvmfs/atlas-nightlies.cern.ch/repo/sw'
+    def status(self, status):
+        """Print status for usage in gitlab-ci."""
+        print 'art-status:', status
 
     def get_script_directory(self):
         """On demand script directory, only to be called if directory exists."""
         if self.script_directory is None:
-            self.script_directory = self.cvmfs_directory
-            self.script_directory = os.path.join(self.script_directory, self.nightly_release, self.nightly_tag, self.project)
-            self.script_directory = os.path.join(self.script_directory, os.listdir(self.script_directory)[0])  # e.g. 21.0.3
-            self.script_directory = os.path.join(self.script_directory, os.listdir(self.script_directory)[0], self.platform)  # InstallArea/x86_64-slc6-gcc62-opt
+            self.script_directory = ArtGrid.CVMFS_DIRECTORY
+            self.script_directory = os.path.join(self.script_directory, self.nightly_release)  # e.g. 21.0
+            self.script_directory = os.path.join(self.script_directory, self.nightly_tag)  # e.g. 2017-10-25T2150
+            self.script_directory = os.path.join(self.script_directory, self.project)  # e.g. Athena
+            try:
+                self.script_directory = os.path.join(self.script_directory, os.listdir(self.script_directory)[0])  # e.g. 21.0.3
+                self.script_directory = os.path.join(self.script_directory, os.listdir(self.script_directory)[0])  # InstallArea
+            except OSError:
+                self.script_directory = os.path.join(self.script_directory, '*', '*')
+            self.script_directory = os.path.join(self.script_directory, self.platform)  # x86_64-slc6-gcc62-opt
         return self.script_directory
 
     def is_script_directory_in_cvmfs(self):
         """Return true if the script directory is in cvmfs."""
-        return self.get_script_directory().startswith(self.cvmfs_directory)
+        return self.get_script_directory().startswith(ArtGrid.CVMFS_DIRECTORY)
+
+    def exit_if_no_script_directory(self):
+        """Exit with ERROR is script directory does not exist."""
+        log = logging.getLogger(MODULE)
+        if not os.path.isdir(self.get_script_directory()):
+            log.critical('Script directory does not exist: %s', self.get_script_directory())
+            self.status('error')
+            exit(1)
+
+    def copy_art(self, run_dir):
+        """Copy all art files to the the run directory. Returns final script directory to be used."""
+        log = logging.getLogger(MODULE)
+        ART = os.path.join(run_dir, "ART")
+        mkdir_p(ART)
+
+        # get the path of the python classes and support scripts
+        art_python_directory = os.path.join(self.art_directory, '..', 'python', 'ART')
+
+        shutil.copy(os.path.join(self.art_directory, 'art.py'), run_dir)
+        shutil.copy(os.path.join(self.art_directory, 'art-get-input.sh'), run_dir)
+        shutil.copy(os.path.join(self.art_directory, 'art-get-tar.sh'), run_dir)
+        shutil.copy(os.path.join(self.art_directory, 'art-internal.py'), run_dir)
+        shutil.copy(os.path.join(art_python_directory, '__init__.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'art_base.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'art_build.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'art_grid.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'art_header.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'art_misc.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'docopt.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'docopt_dispatch.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'parallelScheduler.py'), ART)
+        shutil.copy(os.path.join(art_python_directory, 'serialScheduler.py'), ART)
+
+        make_executable(os.path.join(run_dir, 'art.py'))
+        make_executable(os.path.join(run_dir, 'art-get-input.sh'))
+        make_executable(os.path.join(run_dir, 'art-get-tar.sh'))
+        make_executable(os.path.join(run_dir, 'art-internal.py'))
+
+        script_directory = self.get_script_directory()
+
+        # copy a local test directory if needed (only for 'art grid')
+        if not self.is_script_directory_in_cvmfs():
+            script_directory = os.path.basename(os.path.normpath(self.get_script_directory()))
+            target_directory = os.path.join(run_dir, script_directory)
+            log.info("Copying script directory for grid submission to %s", target_directory)
+            shutil.copytree(self.get_script_directory(), target_directory)
+
+        return script_directory
+
+    def get_jedi_id(self, text):
+        """Return Jedi Task Id or 0."""
+        match = re.search(r"jediTaskID=(\d+)", text)
+        return match.group(1) if match else 0
+
+    def get_nightly_release_short(self):
+        """Return  a short version of the nightly release."""
+        return re.sub(r"-VAL-.*", "-VAL", self.nightly_release)
+
+    def get_outfile(self, user, package, sequence_tag=0, test_name=None, nightly_tag=None):
+        """Create outfile from parameters."""
+        log = logging.getLogger(MODULE)
+
+        if nightly_tag is None:
+            nightly_tag = self.nightly_tag
+
+        if sequence_tag == 0:
+            if not RUCIO:
+                log.critical("RUCIO not available")
+                exit(1)
+
+            scope = '.'.join(('user', user))
+            outfile = '.'.join(('user', user, 'atlas', self.get_nightly_release_short(), self.project, self.platform, nightly_tag, '*', package, 'log'))
+            rucio_client = rucio.client.Client()
+            for out in rucio_client.list_dids(scope, {'name': outfile}):
+                outfile = os.path.splitext(out)[0]
+        else:
+            outfile = '.'.join(('user', user, 'atlas', self.get_nightly_release_short(), self.project, self.platform, nightly_tag, sequence_tag, package))
+        return outfile if test_name is None else '.'.join((outfile, test_name))
+
+    def copy(self, sequence_tag, package, dst, user):
+        """Copy output from scratch area to eos area."""
+        log = logging.getLogger(MODULE)
+        real_user = os.getenv('USER', ArtGrid.ARTPROD)
+        user = real_user if user is None else user
+        default_dst = '/eos/atlas/atlascerngroupdisk/data-art/grid-output' if real_user == ArtGrid.ARTPROD else '.'
+        dst = default_dst if dst is None else dst
+
+        if package is not None:
+            log.info("Copy %s", package)
+            outfile = self.get_outfile(user, package, sequence_tag)
+
+            return self.copy_output(outfile, dst)
+
+        # make sure script directory exist
+        self.exit_if_no_script_directory()
+
+        # get the test_*.sh from the test directory
+        test_directories = self.get_test_directories(self.get_script_directory())
+        if not test_directories:
+            log.warning('No tests found in directories ending in "test"')
+
+        # copy results for all packages
+        result = 0
+        for package, root in test_directories.items():
+            number_of_tests = len(self.get_files(root, "grid", "all", self.nightly_release, self.project, self.platform))
+            if number_of_tests > 0:
+                # FIXME limited
+                if self.nightly_release == '21.0' and package in ['TriggerTest', 'Tier0ChainTests']:
+                    log.info("Copy %s", package)
+                    outfile = self.get_outfile(user, package, sequence_tag)
+
+                    result |= self.copy_output(outfile, dst)
+        return result
+
+    # Not used yet
+    def download(self, did):
+        """Download did into temp directory."""
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
+
+        # rucio downloads cache properly
+        (exit_code, out, err) = run_command("rucio download --dir " + self.rucio_cache + " " + did)
+        if (exit_code != 0):
+            log.error(err)
+        log.info(out)
+        return exit_code
+
+    # Not used yet
+    def get_job_name(self, user, index, package, sequence_tag, nightly_tag):
+        """
+        Return job name for index.
 
-    def task_list(self, job_type, sequence_tag):
+        job_name is without .sh or .py
+        """
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
+
+        outfile = self.get_outfile(user, package, sequence_tag=sequence_tag, nightly_tag=nightly_tag)
+        log.debug("outfile %s", outfile)
+
+        container_json = outfile + ArtGrid.JSON
+        container_log = outfile + ArtGrid.LOG
+        log.info("Downloading json")
+        self.download(container_json)
+        log.info("Downloading log")
+        self.download(container_log)
+
+        index_formatted = index
+        indexed_json = os.path.join(container_json, '.'.join((container_json, sequence_tag, index_formatted, ArtGrid.JSON)))
+        log.debug("Looking for json")
+        if os.path.exists(indexed_json):
+            with open(indexed_json) as json_file:
+                info = json.load(json_file)
+                test_name = os.path.splitext(info['name'])[0]
+                return test_name
+
+        indexed_log = os.path.join(container_log, '.'.join((container_log, sequence_tag, index_formatted, ArtGrid.LOG_TGZ)))
+        log.debug("Looking for log")
+        if os.path.exists(indexed_log):
+            tar = tarfile.open(indexed_log)
+            for name in tar.getnames():
+                if ArtGrid.ATHENA_STDOUT in name:
+                    log.debug("Found %s", ArtGrid.ATHENA_STDOUT)
+                    info = tar.extractfile(name).read()
+                    # try art-job-name
+                    match = re.search(r"art-job-name:\s(\S+)", info)
+                    if match:
+                        log.debug("Found 'art-job-name'")
+                        return os.path.splitext(match.group(1))[0]
+
+                    # try Job Name
+                    match = re.search(r"Job Name:\s(\S+)", info)
+                    if match:
+                        log.debug("Found 'Job Name:'")
+                        return os.path.splitext(match.group(1))[0]
+
+        log.error("Cannot retrieve job_name from art-job.json or logfile")
+        return None
+
+    def get_test_name(self, rucio_name, rucio_log_name):
+        """Return test_name for log rucio_name."""
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
+
+        tmp_dir = tempfile.mkdtemp()
+        atexit.register(shutil.rmtree, tmp_dir)
+
+        tmp_json = os.path.join(tmp_dir, ArtGrid.ART_JOB)
+
+        if rucio_name is not None:
+            (exit_code, out, err) = run_command(' '.join(('xrdcp -N -f ', rucio_name, tmp_json)))
+            if exit_code == 0:
+                log.debug("copied json %s", rucio_name)
+                with open(tmp_json) as json_file:
+                    info = json.load(json_file)
+                    test_name = os.path.splitext(info['name'])[0]
+                    return test_name
+
+        tmp_log = os.path.join(tmp_dir, ArtGrid.LOG_TGZ)
+
+        if rucio_log_name is not None:
+            (exit_code, out, err) = run_command(' '.join(('xrdcp -N -f ', rucio_log_name, tmp_log)))
+            if exit_code == 0:
+                log.debug("copied log %s %s", rucio_log_name, tmp_log)
+                tar = tarfile.open(tmp_log)
+                for name in tar.getnames():
+                    if ArtGrid.ATHENA_STDOUT in name:
+                        log.debug("Found %s", ArtGrid.ATHENA_STDOUT)
+                        info = tar.extractfile(name).read()
+                        # try art-job-name
+                        match = re.search(r"art-job-name:\s(\S+)", info)
+                        if match:
+                            log.debug("Found 'art-job-name'")
+                            return os.path.splitext(match.group(1))[0]
+
+                        # try Job Name
+                        match = re.search(r"Job Name:\s(\S+)", info)
+                        if match:
+                            log.debug("Found 'Job Name:'")
+                            return os.path.splitext(match.group(1))[0]
+
+        log.debug("Cannot retrieve job_name from art-job.json or logfile")
+        return None
+
+    def copy_output(self, outfile, dst):
+        """Copy outfile to dst."""
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
+
+        result = 0
+        outfile_pattern = r"([^\.]+)\.([^\.]+)\.([^\.]+)\.(.+)\.([^\.]+)\.([^\.]+)\.([^\.]+)\.([^\.]+)\.([^\.\n]+)"
+        match = re.search(outfile_pattern, outfile)
+        if not match:
+            log.error("%s does not match pattern", outfile)
+            return 1
+        (user_type, user, experiment, nightly_release, project, platform, nightly_tag, sequence_tag, package) = match.groups()
+        dst_dir = os.path.join(dst, nightly_release, nightly_tag, project, platform, package)
+        log.info(dst_dir)
+
+        scope = '.'.join((user_type, user))
+
+        tmp_dir = tempfile.mkdtemp()
+        atexit.register(shutil.rmtree, tmp_dir)
+
+        tmp_json = os.path.join(tmp_dir, ArtGrid.ART_JOB)
+        tmp_log = os.path.join(tmp_dir, ArtGrid.LOG_TGZ)
+        tmp_tar = os.path.join(tmp_dir, ArtGrid.JOB_TAR)
+
+        jsons = self.get_rucio_map(scope, outfile, ArtGrid.JSON)
+        logs = self.get_rucio_map(scope, outfile, ArtGrid.LOG)
+        tars = self.get_rucio_map(scope, outfile, ArtGrid.OUTPUT)
+
+        # log.debug(jsons)
+        # log.debug(logs)
+
+        for number in tars:
+            # get the test name
+            rucio_name = jsons[number]['rucio_name'] if number in jsons else None
+            rucio_log_name = logs[number]['rucio_name'] if number in logs else None
+            test_name = self.get_test_name(rucio_name, rucio_log_name)
+            if test_name is None:
+                log.error("JSON Lookup Error for test %s", rucio_name)
+                result = 1
+                continue
+
+            # create tmp test directory
+            test_dir = os.path.join(tmp_dir, test_name)
+            mkdir_p(test_dir)
+
+            # copy art-job in, ignore error
+            run_command(' '.join(('xrdcp -N -f', rucio_name, tmp_json)))
+            shutil.copyfile(tmp_json, os.path.join(test_dir, ArtGrid.ART_JOB))
+
+            # copy and unpack log
+            log_source = logs[number]['source']
+            (exit_code, out, err) = run_command(' '.join(('xrdcp -N -f', rucio_log_name, tmp_log)))
+            if exit_code != 0:
+                log.error("Log Unpack Error: %d %s %s", exit_code, out, err)
+                result = 1
+            else:
+                tar = tarfile.open(tmp_log)
+                for member in tar.getmembers():
+                    tar.extract(member, path=test_dir)
+                # does not work: tar.extractall()
+                tar.close()
+
+            log.info("Copying: %d %s", number, test_name)
+            log.info("- json: %s", jsons[number]['source'])
+            log.info("- log:  %s", log_source)
+            log.info("- tar:  %s", tars[number]['source'])
+
+            # copy results and unpack
+            (exit_code, out, err) = run_command(' '.join(('xrdcp -N -f', tars[number]['rucio_name'], tmp_tar)))
+            if exit_code != 0:
+                log.error("TAR Error: %d %s %s", exit_code, out, err)
+                result = 1
+            else:
+                tar = tarfile.open(tmp_tar)
+                tar.extractall(path=test_dir)
+                tar.close()
+
+            # copy to eos
+            dst_target = os.path.join(dst_dir, test_name)
+            log.info("to: %s", dst_target)
+            if dst_target.startswith('/eos'):
+                mkdir_cmd = 'eos mkdir -p'
+                xrdcp_target = 'root://eosatlas.cern.ch/' + dst_target
+            else:
+                mkdir_cmd = 'mkdir -p'
+                xrdcp_target = dst_target
+
+            (exit_code, out, err) = run_command(' '.join((mkdir_cmd, dst_target)))
+            if exit_code != 0:
+                log.error("Mkdir Error: %d %s %s", exit_code, out, err)
+                result = 1
+            else:
+                (exit_code, out, err) = run_command(' '.join(('xrdcp -N -r -v', test_dir, xrdcp_target)))
+                if exit_code not in [0, 51, 54]:
+                    # 0 all is ok
+                    # 51 File exists
+                    # 54 is already copied
+                    log.error("XRDCP to EOS Error: %d %s %s", exit_code, out, err)
+                    result = 1
+
+            # cleanup
+            shutil.rmtree(test_dir)
+
+        return result
+
+    def get_rucio_map(self, scope, outfile, extension):
+        """Return map of entries by grid_index into { source, rucio_name }."""
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
+
+        CERN = 'CERN-PROD_SCRATCHDISK'
+
+        LOG_PATTERN = r"\.(\d{6})\.log\.tgz"
+        JSON_PATTERN = r"\._(\d{6})\.art-job\.json"
+        OUTPUT_PATTERN = r"\._(\d{6})\.tar"
+        table = {}
+        rucio_client = rucio.client.Client()
+        log.debug("Looking for %s", outfile + extension)
+        for rep in rucio_client.list_replicas([{'scope': scope, 'name': outfile + extension}], schemes=['root']):
+            source = None
+            rucio_name = None
+            log.debug("Found in %s", rep['states'].keys())
+            # first look at CERN
+            if CERN in rep['states'].keys() and rep['states'][CERN] == 'AVAILABLE':
+                source = CERN
+                rucio_name = rep['rses'][CERN][0]
+            else:
+                for rse in rep['states'].keys():
+                    if rep['states'][rse] == 'AVAILABLE' and len(rep['rses'][rse]) >= 1:
+                        source = rse
+                        rucio_name = rep['rses'][rse][0]
+                        break
+
+            # maybe not found at all
+            if rucio_name is not None:
+                log.debug("Found rucio name %s in %s", rucio_name, source)
+                pattern = JSON_PATTERN if extension == ArtGrid.JSON else LOG_PATTERN if extension == ArtGrid.LOG else OUTPUT_PATTERN
+                match = re.search(pattern, rucio_name)
+                if match:
+                    number = int(match.group(1))
+                else:
+                    log.warning("%s does not contain test number using pattern %s skipped...", rucio_name, pattern)
+                    continue
+
+                table[number] = {'source': source, 'rucio_name': rucio_name}
+
+        if not table:
+            log.warning("Outfile %s not found or empty", outfile + extension)
+        return table
+
+    def task_package(self, root, package, job_type, sequence_tag, no_action):
+        """TBD."""
+        log = logging.getLogger(MODULE)
+        result = {}
+        number_of_tests = len(self.get_files(root, job_type, "all", self.nightly_release, self.project, self.platform))
+        if number_of_tests > 0:
+            print 'art-package:', package
+            self.status('included')
+            log.info('root %s', root)
+            log.info('Handling %s for %s project %s on %s', package, self.nightly_release, self.project, self.platform)
+            log.info("Number of tests: %d", number_of_tests)
+            submit_dir = os.path.join(self.submit_directory, package)
+            run_dir = os.path.join(submit_dir, "run")
+
+            script_directory = self.copy_art(run_dir)
+
+            result = self.task(script_directory, package, job_type, sequence_tag, no_action)
+        return result
+
+    def task_list(self, job_type, sequence_tag, package=None, no_action=False):
         """TBD."""
+        log = logging.getLogger(MODULE)
         # job will be submitted from tmp directory
-        submit_directory = tempfile.mkdtemp(dir='.')
+        self.submit_directory = tempfile.mkdtemp(dir='.')
 
         # make sure tmp is removed afterwards
-        atexit.register(shutil.rmtree, submit_directory)
+        atexit.register(shutil.rmtree, self.submit_directory)
 
         # make sure script directory exist
-        if not os.path.isdir(self.get_script_directory()):
-            print 'ERROR: script directory does not exist:', self.get_script_directory()
-            print 'art-status: error'
-            sys.stdout.flush()
-            exit(1)
+        self.exit_if_no_script_directory()
 
         # get the test_*.sh from the test directory
         test_directories = self.get_test_directories(self.get_script_directory())
         if not test_directories:
-            print 'WARNING: No tests found in directories ending in "test"'
-            sys.stdout.flush()
+            log.warning('No tests found in directories ending in "test"')
 
-        for package, root in test_directories.items():
-            if package in ['TrigInDetValidation']:
-                continue
+        all_results = {}
+
+        if package is None:
+            config = None if self.skip_setup else self.get_config()
+            excluded_packages = config.get('excluded-packages', []) if config is not None else []
+
+            # submit tasks for all packages
+            for package, root in test_directories.items():
+                if package in excluded_packages:
+                    log.warning("Package %s is excluded", package)
+                else:
+                    all_results.update(self.task_package(root, package, job_type, sequence_tag, no_action))
+        else:
+            # Submit single package
+            root = test_directories[package]
+            all_results.update(self.task_package(root, package, job_type, sequence_tag, no_action))
 
-            number_of_tests = len(self.get_files(root, job_type, "all", self.nightly_release, self.project, self.platform))
-            if number_of_tests > 0:
-                print 'art-package: ' + package
-                print 'art-status: included'
-                print 'root' + root
-                print 'Handling', package, 'for', self.nightly_release, 'project', self.project, 'on', self.platform
-                print "Number of tests:", str(number_of_tests)
-                sys.stdout.flush()
-                submit_dir = os.path.join(submit_directory, package)
-                run = os.path.join(submit_dir, "run")
-                ART = os.path.join(run, "ART")
-                mkdir_p(ART)
-
-                # get the path of the python classes and support scripts
-                art_python_directory = os.path.join(self.art_directory, '..', 'python', 'ART')
-
-                shutil.copy(os.path.join(self.art_directory, 'art.py'), run)
-                shutil.copy(os.path.join(self.art_directory, 'art-get-input.sh'), run)
-                shutil.copy(os.path.join(self.art_directory, 'art-get-tar.sh'), run)
-                shutil.copy(os.path.join(self.art_directory, 'art-internal.py'), run)
-                shutil.copy(os.path.join(art_python_directory, '__init__.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'art_base.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'art_build.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'art_grid.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'art_header.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'art_misc.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'docopt.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'docopt_dispatch.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'parallelScheduler.py'), ART)
-                shutil.copy(os.path.join(art_python_directory, 'serialScheduler.py'), ART)
-
-                make_executable(os.path.join(run, 'art.py'))
-                make_executable(os.path.join(run, 'art-get-input.sh'))
-                make_executable(os.path.join(run, 'art-get-tar.sh'))
-                make_executable(os.path.join(run, 'art-internal.py'))
-
-                # copy a local test directory if needed (only for 'art grid')
-                if not self.is_script_directory_in_cvmfs():
-                    local_test_dir = os.path.join(run, os.path.basename(os.path.normpath(self.get_script_directory())))
-                    print "Copying script directory for grid submission to ", local_test_dir
-                    shutil.copytree(self.get_script_directory(), local_test_dir)
-
-                command = ' '.join((os.path.join(self.art_directory, 'art-internal.py'), 'task', 'grid', '--skip-setup' if self.skip_setup else '', submit_directory, self.get_script_directory(), package, job_type, sequence_tag, self.nightly_release, self.project, self.platform, self.nightly_tag))
-                print command
-                sys.stdout.flush()
-
-                env = os.environ.copy()
-                env['PATH'] = '.:' + env['PATH']
-                out = exit_on_failure(run_command(command, env=env))
-                print out
-                sys.stdout.flush()
         return 0
 
-    def task(self, package, job_type, sequence_tag):
-        """TBD."""
-        print 'Running art task'
-        sys.stdout.flush()
+    def task(self, script_directory, package, job_type, sequence_tag, no_action=False):
+        """
+        Submit a task, consisting of multiple jobs.
+
+        For 'single' jobs each task contains exactly one job.
+        Returns a map of JediIds to tuples of (package, test_name)
+        """
+        log = logging.getLogger(MODULE)
+        log.info('Running art task')
 
         config = None if self.skip_setup else self.get_config()
         grid_options = self.grid_option(config, package, 'grid-exclude-sites', '--excludedSite=')
@@ -143,28 +541,37 @@ class ArtGrid(ArtBase):
         env['PATH'] = '.:' + env['PATH']
         env['ART_GRID_OPTIONS'] = grid_options
 
-        test_directories = self.get_test_directories(self.get_script_directory())
+        test_directories = self.get_test_directories(script_directory)
         test_directory = test_directories[package]
         number_of_batch_tests = len(self.get_files(test_directory, job_type, "batch", self.nightly_release, self.project, self.platform))
 
         MAX_OUTFILE_LEN = 132
-        user = env['USER'] if self.skip_setup else 'artprod'
-        nightly_release_short = re.sub(r"-VAL-.*", "-VAL", self.nightly_release)
-        outfile = '.'.join(('user', user, 'atlas', nightly_release_short, self.project, self.platform, self.nightly_tag, sequence_tag, package))
+        user = env['USER'] if self.skip_setup else ArtGrid.ARTPROD
+        outfile = self.get_outfile(user, package, sequence_tag)
+
+        result = {}
 
         # submit batch tests
         if number_of_batch_tests > 0:
             if len(outfile) > MAX_OUTFILE_LEN:
-                print "ERROR: OutFile string length >", MAX_OUTFILE_LEN, ": ", outfile
-                sys.stdout.flush()
+                log.error("OutFile string length > %d: ", MAX_OUTFILE_LEN, outfile)
                 return 1
 
-            print "batch:", nightly_release_short, self.project, self.platform, self.nightly_tag, sequence_tag, package, job_type, str(number_of_batch_tests), grid_options
-            sys.stdout.flush()
-
-            out = exit_on_failure(run_command(' '.join((os.path.join(self.art_directory, 'art-task-grid.sh'), '--skip-setup' if self.skip_setup else '', self.submit_directory, self.get_script_directory(), package, job_type, sequence_tag, str(number_of_batch_tests), nightly_release_short, self.project, self.platform, self.nightly_tag, outfile)), env=env))
-            print out
-            sys.stdout.flush()
+            # Batch
+            cmd = ' '.join((os.path.join(self.art_directory, 'art-task-grid.sh'), '--skip-setup' if self.skip_setup else '', self.submit_directory, script_directory, package, job_type, sequence_tag, str(number_of_batch_tests), self.get_nightly_release_short(), self.project, self.platform, self.nightly_tag, outfile))
+            log.info("batch: %s", cmd)
+
+            if not no_action:
+                (exit_code, out, err) = run_command(cmd, env=env)
+                if exit_code != 0:
+                    log.error("art-task-grid failed %d", exit_code)
+                    print out
+                    print err
+                else:
+                    jediID = self.get_jedi_id(err)
+                    if jediID > 0:
+                        result[jediID] = (package, "", outfile)
+                log.info(out)
 
         # submit single tests
         index = 1
@@ -173,34 +580,40 @@ class ArtGrid(ArtBase):
             header = ArtHeader(job)
             inds = header.get('art-input')
             nFiles = header.get('art-input-nfiles')
-            nEvents = header.get('art-input-nevents')
-            files = ','.join(header.get('art-input-file'))
             split = header.get('art-input-split')
 
-            outfile_test = '.'.join((outfile, str(index)))
+            outfile_test = self.get_outfile(user, package, sequence_tag, str(index))
             if len(outfile_test) > MAX_OUTFILE_LEN:
-                print "ERROR: OutFile string length >", MAX_OUTFILE_LEN, ": ", outfile_test
-                sys.stdout.flush()
+                log.error("ERROR: OutFile string length > %d : %s ", MAX_OUTFILE_LEN, outfile_test)
                 return 1
 
-            print "single:", nightly_release_short, self.project, self.platform, self.nightly_tag, sequence_tag, package, job_type, str(split), inds, str(nFiles), str(nEvents), grid_options
-            sys.stdout.flush()
+            # Single
+            cmd = ' '.join((os.path.join(self.art_directory, 'art-task-grid.sh'), '--skip-setup' if self.skip_setup else '', '--test-name ' + test_name, '--inDS ' + inds, '--nFiles ' + str(nFiles) if nFiles > 0 else '', self.submit_directory, script_directory, package, job_type, sequence_tag, str(split), self.get_nightly_release_short(), self.project, self.platform, self.nightly_tag, outfile_test))
+            log.info("single: %s", cmd)
 
-            out = exit_on_failure(run_command(' '.join((os.path.join(self.art_directory, 'art-task-grid.sh'), '--skip-setup' if self.skip_setup else '', '--test-name ' + test_name, '--inDS ' + inds, '--nFiles ' + str(nFiles) if nFiles > 0 else '', '--nEvents ' + str(nEvents) if nEvents > 0 else '', '--fileList ' + files if files != '' else '', self.submit_directory, self.get_script_directory(), package, job_type, sequence_tag, str(split), nightly_release_short, self.project, self.platform, self.nightly_tag, outfile_test)), env=env))
-            print out
-            sys.stdout.flush()
+            if not no_action:
+                (exit_code, out, err) = run_command(cmd, env=env)
+                if exit_code != 0:
+                    log.error("art-task-grid failed %d", exit_code)
+                    print out
+                    print err
+                else:
+                    jediID = self.get_jedi_id(err)
+                    if jediID > 0:
+                        result[jediID] = (package, test_name, outfile_test)
+
+                log.info(out)
 
             index += 1
 
-        return 0
+        return result
 
     def job(self, package, job_type, sequence_tag, index_type, index_or_name, out):
         """TBD."""
-        print 'Running art job grid'
-        sys.stdout.flush()
+        log = logging.getLogger(MODULE)
+        log.info('Running art job grid')
 
-        print self.nightly_release, self.project, self.platform, self.nightly_tag, package, job_type, str(index_or_name), out
-        sys.stdout.flush()
+        log.info("%s %s %s %s %s %s %s %s", self.nightly_release, self.project, self.platform, self.nightly_tag, package, job_type, str(index_or_name), out)
 
         test_directories = self.get_test_directories(self.get_script_directory())
         test_directory = test_directories[package]
@@ -213,22 +626,26 @@ class ArtGrid(ArtBase):
         else:
             test_name = index_or_name
 
+        log.info("art-job-name: %s", test_name)
+
         test_file = os.path.join(test_directory, test_name)
+        # arguments are SCRIPT_DIRECTORY, PACKAGE, TYPE, TEST_NAME, NIGHTLY_RELEASE, PROJECT, PLATFORM, NIGHTLY_TAG
         command = ' '.join((test_file, self.get_script_directory(), package, job_type, test_name, self.nightly_release, self.project, self.platform, self.nightly_tag))
 
-        print test_name
-        print test_directory
-        print command
-        sys.stdout.flush()
+        log.debug(test_name)
+        log.debug(test_directory)
+        log.debug(command)
 
         # run the test
         env = os.environ.copy()
         env['PATH'] = '.:' + env['PATH']
         (exit_code, output, error) = run_command(command, env=env)
+        print output
         if (exit_code != 0):
+            log.error("Test %s failed %d", str(index_or_name), exit_code)
             print error
-        print output
-        sys.stdout.flush()
+        # NOTE: exit_code always 0
+        print error
 
         # gather results
         result = {}
@@ -247,8 +664,20 @@ class ArtGrid(ArtBase):
                 result['result'].append(item)
 
         # write out results
-        with open(os.path.join("art-job.json"), 'w') as jobfile:
+        with open(os.path.join(ArtGrid.ART_JOB), 'w') as jobfile:
             json.dump(result, jobfile, sort_keys=True, indent=4, ensure_ascii=False)
+            log.info("Wrote %s", ArtGrid.ART_JOB)
+
+        # grab the content of "jobReport.json", add the art dictionary and write it back
+        if os.path.isfile(ArtGrid.JOB_REPORT):
+            with open(ArtGrid.JOB_REPORT, 'r+') as json_file:
+                info = json.load(json_file)
+                info[ArtGrid.JOB_REPORT_ART_KEY] = result
+                # write out results
+                json_file.seek(0)
+                json.dump(info, json_file, sort_keys=True, indent=4, ensure_ascii=False)
+                json_file.truncate()
+                log.info("Updated %s", ArtGrid.JOB_REPORT)
 
         # pick up the outputs
         tar_file = tarfile.open(out, mode='w')
@@ -259,42 +688,97 @@ class ArtGrid(ArtBase):
                 # remove comments
                 line = line.split('#', 1)[0]
                 out_names = re.findall(r"--output[^\s=]*[= ]*(\S*)", line)
-                print out_names
+                log.debug(out_names)
                 for out_name in out_names:
                     out_name = out_name.strip('\'"')
                     if os.path.exists(out_name):
-                        print 'Tar file contain: ', out_name
+                        log.info('Tar file contain: %s', out_name)
                         tar_file.add(out_name)
 
         # pick up art-header named outputs
         for path_name in ArtHeader(test_file).get('art-output'):
             for out_name in glob.glob(path_name):
-                print 'Tar file contains:', out_name
+                log.info('Tar file contains: %s', out_name)
                 tar_file.add(out_name)
 
         tar_file.close()
         # Always return 0
         return 0
 
-    def list(self, package, job_type, index_type, json_format=False):
+    def get_grid_map(self, user, package, sequence_tag=0, nightly_tag=None):
+        """Return grid map of test_name to grid_index."""
+        log = logging.getLogger(MODULE)
+        scope = '.'.join(('user', user))
+
+        outfile = self.get_outfile(user, package, sequence_tag=sequence_tag, nightly_tag=nightly_tag)
+        log.debug("outfile %s", outfile)
+        jsons = self.get_rucio_map(scope, outfile, ArtGrid.JSON)
+        logs = self.get_rucio_map(scope, outfile, ArtGrid.LOG)
+
+        result = {}
+        for grid_index in logs:
+            rucio_name = jsons[grid_index]['rucio_name'] if grid_index in jsons else None
+            rucio_log_name = logs[grid_index]['rucio_name'] if grid_index in logs else None
+            test_name = self.get_test_name(rucio_name, rucio_log_name)
+            if test_name is None:
+                # log.warning("JSON Lookup failed for test %s", rucio_log_name if rucio_name is None else rucio_name)
+                continue
+
+            result[test_name] = int(grid_index)
+        return result
+
+    def list(self, package, job_type, index_type, json_format, user):
         """TBD."""
-        jobs = self.get_list(self.get_script_directory(), package, job_type, index_type)
+        log = logging.getLogger(MODULE)
+        user = ArtGrid.ARTPROD if user is None else user
+
+        # make sure script directory exist
+        self.exit_if_no_script_directory()
+
+        log.info("Getting grid map...")
+        grid_map = self.get_grid_map(user, package)
+
+        log.info("Getting test names...")
+        test_names = self.get_list(self.get_script_directory(), package, job_type, index_type)
+        json_array = []
+        for test_name in test_names:
+            name = os.path.splitext(test_name)[0]
+            json_array.append({
+                'name': name,
+                'grid_index': str(grid_map[name]) if name in grid_map else '-1'
+            })
+
         if json_format:
-            json.dump(jobs, sys.stdout, sort_keys=True, indent=4)
+            json.dump(json_array, sys.stdout, sort_keys=True, indent=4)
             return 0
-        i = 1
-        for job in jobs:
-            print str(i) + ' ' + job
+
+        i = 0
+        for entry in json_array:
+            print str(i) + ' ' + entry['name'] + (' ' + entry['grid_index'])
             i += 1
-        sys.stdout.flush()
+
+        # print warnings
+        for entry in json_array:
+            if entry['grid_index'] < 0:
+                log.warning('test %s could not be found in json or log', entry['name'])
+
         return 0
 
-    def log(self, package, test_name):
+    def log(self, package, test_name, user):
         """TBD."""
-        tar = self.get_tar(package, test_name, '.log')
+        log = logging.getLogger(MODULE)
+        user = ArtGrid.ARTPROD if user is None else user
+
+        # make sure script directory exist
+        self.exit_if_no_script_directory()
+
+        tar = self.open_tar(user, package, test_name, ArtGrid.LOG)
+        if tar is None:
+            log.error("No log tar file found")
+            return 1
 
         for name in tar.getnames():
-            if 'athena_stdout.txt' in name:
+            if ArtGrid.ATHENA_STDOUT in name:
                 f = tar.extractfile(name)
                 content = f.read()
                 print content
@@ -302,49 +786,64 @@ class ArtGrid(ArtBase):
         tar.close()
         return 0
 
-    def output(self, package, test_name, file_name):
+    def output(self, package, test_name, user):
         """TBD."""
-        tar = self.get_tar(package, test_name, '_EXT1')
+        log = logging.getLogger(MODULE)
+        user = ArtGrid.ARTPROD if user is None else user
 
-        for member in tar.getmembers():
-            if file_name in member.name:
-                tar.extractall(path='.', members=[member])
-                break
+        # make sure script directory exist
+        self.exit_if_no_script_directory()
+
+        outfile = self.get_outfile(user, package)
+        tar_dir = os.path.join(tempfile.gettempdir(), outfile + ArtGrid.OUTPUT)
+        mkdir_p(tar_dir)
+
+        tar = self.open_tar(user, package, test_name, ArtGrid.OUTPUT)
+        if tar is None:
+            log.error("No output tar file found")
+            return 1
+
+        tar.extractall(path=tar_dir)
         tar.close()
+        print "Output extracted in", tar_dir
         return 0
 
-    def compare(self, package, test_name, days, file_names):
+    def compare(self, package, test_name, days, file_names, user):
         """TBD."""
+        log = logging.getLogger(MODULE)
+        user = ArtGrid.ARTPROD if user is None else user
+
         previous_nightly_tag = self.get_previous_nightly_tag(days)
-        print "Previous Nightly Tag:", str(previous_nightly_tag)
+        log.info("LOG Previous Nightly Tag: %s", str(previous_nightly_tag))
+        print "PRINT Previous Nightly Tag", str(previous_nightly_tag)
+
         if previous_nightly_tag is None:
-            print "ERROR: No previous nightly tag found"
-            # do not flag as error, to make sure tar file gets uploaded
-            return 0
+            log.error("No previous nightly tag found")
+            return 1
 
         ref_dir = os.path.join('.', 'ref-' + previous_nightly_tag)
         mkdir_p(ref_dir)
 
-        tar = self.get_tar(package, test_name, '_EXT1', previous_nightly_tag)
+        tar = self.open_tar(user, package, test_name, ArtGrid.OUTPUT, previous_nightly_tag)
         if tar is None:
-            print "ERROR: No comparison tar file found"
-            # do not flag as error, to make sure tar file gets uploaded
-            return 0
+            log.error("No comparison tar file found")
+            return 1
 
         for member in tar.getmembers():
             if member.name in file_names:
                 tar.extractall(path=ref_dir, members=[member])
         tar.close()
 
+        result = 0
         for file_name in file_names:
             ref_file = os.path.join(ref_dir, file_name)
             if os.path.isfile(ref_file):
                 print "art-compare:", previous_nightly_tag, file_name
-                self.compare_ref(file_name, ref_file, 10)
+                result |= self.compare_ref(file_name, ref_file, 10)
             else:
-                print "ERROR:", ref_file, "not found in tar file"
-        # do not flag as error, to make sure tar file gets uploaded
-        return 0
+                log.error("%s not found in tar file", ref_file)
+                result = 1
+        return result
 
     def grid_option(self, config, package, key, option_key):
         """Based on config, return value for key, or ''.
@@ -365,67 +864,49 @@ class ArtGrid(ArtBase):
         else:
             return option_key + global_value + ('' if value is None else ', ' + value)
 
-    def get_tar(self, package, test_name, extension, nightly_tag=None):
+    def open_tar(self, user, package, test_name, extension, nightly_tag=None):
         """Open tar file for particular release."""
-        if nightly_tag is None:
-            nightly_tag = self.nightly_tag
-
-        try:
-            job_type = self.get_type(self.get_test_directories(self.get_script_directory())[package], test_name)
-            print "Job Type:", job_type
-            files = self.get_list(self.get_script_directory(), package, job_type, "batch")
-            number_of_tests = len(files)
-            index = files.index(test_name)
-            print "Index:", index
-        except KeyError:
-            print package, "does not exist in tests of ", self.get_script_directory()
-            return None
+        log = logging.getLogger(MODULE)
+        if not RUCIO:
+            log.critical("RUCIO not available")
+            exit(1)
 
-        try:
-            tmpdir = os.environ['TMPDIR']
-        except KeyError:
-            tmpdir = '.'
-        print "Using", tmpdir, "for tar file download"
+        log.info("Getting grid map...")
+        grid_map = self.get_grid_map(user, package, nightly_tag=nightly_tag)
 
-        # run in correct environment
-        env = os.environ.copy()
-        env['PATH'] = '.:' + env['PATH']
+        name = os.path.splitext(test_name)[0]
+        if name not in grid_map:
+            log.error("No log or tar found for package %s or test %s", package, test_name)
+            return None
 
-        # Grid counts from 1, retries will up that number every time by total number of jobs
-        index += 1
-        retries = 3
-        retry = 0
+        grid_index = grid_map[name]
+        log.info("Grid Index: %d", grid_index)
 
-        (code, out, err) = (0, "", "")
-        while retry < retries:
-            try_index = (retry * number_of_tests) + index
-            print (retry + 1), ": Get tar for index ", try_index
-            # run art-get-tar.sh
-            (code, out, err) = run_command(' '.join((os.path.join(self.art_directory, "art-get-tar.sh"), package, str(try_index), "_EXT1", self.nightly_release, self.project, self.platform, nightly_tag)), dir=tmpdir, env=env)
-            if code == 0 and out != '':
+        scope = '.'.join(('user', user))
+        outfile = self.get_outfile(user, package, nightly_tag=nightly_tag)
+        rucio_map = self.get_rucio_map(scope, outfile, extension)
+        if grid_index not in rucio_map:
+            log.error("No entry in rucio map for %d", grid_index)
+            return None
 
-                match = re.search(r"TAR_NAME=(.*)", out)
-                if match:
-                    tar_name = match.group(1)
-                    print "Matched TAR_NAME ", tar_name
+        rucio_name = rucio_map[grid_index]['rucio_name']
+        log.info("RUCIO: %s", rucio_name)
 
-                    if tar_name != "":
-                        print "Tar Name:", tar_name
-                        break
+        tmp_dir = tempfile.mkdtemp()
+        atexit.register(shutil.rmtree, tmp_dir)
 
-            retry += 1
+        tmp_tar = os.path.join(tmp_dir, os.path.basename(rucio_name))
 
-        if retry >= retries:
-            print "Code:", str(code)
-            print "Err:", err
-            print "Out:", out
+        (exit_code, out, err) = run_command(' '.join(('xrdcp -N -f', rucio_name, tmp_dir)))
+        if exit_code != 0:
+            log.error("TAR Error: %s %d %s %s", rucio_name, exit_code, out, err)
             return None
 
-        return tarfile.open(os.path.join(tmpdir, tar_name.replace(':', '/', 1)))
+        return tarfile.open(tmp_tar)
 
     def get_previous_nightly_tag(self, days):
         """TBD. 21:00 is cutoff time."""
-        directory = os.path.join(self.cvmfs_directory, self.nightly_release)
+        directory = os.path.join(ArtGrid.CVMFS_DIRECTORY, self.nightly_release)
         tags = os.listdir(directory)
         tags.sort(reverse=True)
         tags = [x for x in tags if re.match(r'\d{4}-\d{2}-\d{2}T\d{2}\d{2}', x)]
diff --git a/Tools/ART/python/ART/art_header.py b/Tools/ART/python/ART/art_header.py
index 046de1b53d7ae113a96cd6ffd266501063149acd..372cff4983283e1a84f02bd170b155ffc3cb8c47 100644
--- a/Tools/ART/python/ART/art_header.py
+++ b/Tools/ART/python/ART/art_header.py
@@ -4,12 +4,15 @@
 
 __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
 
+import logging
 import re
 
 from types import IntType
 from types import ListType
 from types import StringType
 
+MODULE = "art.header"
+
 
 class ArtHeader(object):
     """TBD."""
@@ -36,9 +39,7 @@ class ArtHeader(object):
         self.add('art-include', ListType, ['*'])
         self.add('art-output', ListType, [])
         self.add('art-input', StringType, None)
-        self.add('art-input-file', ListType, [])
-        self.add('art-input-nfiles', IntType, 0)
-        self.add('art-input-nevents', IntType, 0)
+        self.add('art-input-nfiles', IntType, 1)
         self.add('art-input-split', IntType, 0)
 
         self.read(filename)
@@ -49,7 +50,7 @@ class ArtHeader(object):
         self.header[key]['type'] = value_type
         self.header[key]['default'] = default_value
         self.header[key]['constraint'] = constraint
-        self.header[key]['value'] = None    # never set
+        self.header[key]['value'] = None    # e.g. the value was never set
 
     def is_list(self, key):
         """TBD."""
@@ -57,26 +58,38 @@ class ArtHeader(object):
 
     def read(self, filename):
         """Read all headers from file."""
+        log = logging.getLogger(MODULE)
         for line in open(filename, "r"):
             line_match = self.header_format.match(line)
             if line_match:
-                key = line_match.group(1)
-                value = line_match.group(2)
-                if key in self.header and self.header[key]['type'] == StringType:
-                    value = value.strip()
-
-                if self.is_list(key):
-                    if self.header[key]['value'] is None:
-                        self.header[key]['value'] = []
-                    self.header[key]['value'].append(value)
-                else:
-                    if key not in self.header:
-                        self.header[key] = {}
-                    self.header[key]['value'] = value
+                try:
+                    key = line_match.group(1)
+                    value = line_match.group(2)
+                    if key in self.header:
+                        if self.header[key]['type'] == StringType:
+                            value = value.strip()
+                        elif self.header[key]['type'] == IntType:
+                            value = int(value)
+
+                    if self.is_list(key):
+                        # handle list types
+                        if self.header[key]['value'] is None:
+                            self.header[key]['value'] = []
+                        self.header[key]['value'].append(value)
+                    else:
+                        # handle values
+                        if key not in self.header:
+                            log.warning("Unknown art-header %s: %s in file %s", key, value, filename)
+                            self.header[key] = {}
+                        self.header[key]['value'] = value
+                except ValueError:
+                    log.error("Invalid value in art-header %s: %s in file %s", key, value, filename)
 
     def get(self, key):
         """TBD."""
+        log = logging.getLogger(MODULE)
         if key not in self.header:
+            log.warning("Art seems to look for a header key %s which is not in the list of defined headers.", key)
             return None
 
         if self.header[key]['value'] is None:
@@ -86,8 +99,9 @@ class ArtHeader(object):
 
     def print_it(self):
         """TBD."""
+        log = logging.getLogger(MODULE)
         for key in self.header:
-            print key, self.header[key]['type'], self.header[key]['default'], self.header[key]['value'], self.header[key]['constraint']
+            log.info("%s: %s %s %s %s", key, self.header[key]['type'], self.header[key]['default'], self.header[key]['value'], self.header[key]['constraint'])
 
     def validate(self):
         """
@@ -100,34 +114,35 @@ class ArtHeader(object):
         - a value is found of the wrong value_type
         - a value is found outside the constraint
         """
+        log = logging.getLogger(MODULE)
         for line in open(self.filename, "r"):
             if self.header_format_error1.match(line):
-                print "LINE: ", line.rstrip()
-                print "ERROR: Header Validation - invalid header format, use space between '# and art-xxx' in file", self.filename
-                print
+                log.error("LINE: %s", line.rstrip())
+                log.error("Header Validation - invalid header format, use space between '# and art-xxx' in file %s", self.filename)
+                log.error("")
             if self.header_format_error2.match(line):
-                print "LINE: ", line.rstrip()
-                print "ERROR: Header Validation - invalid header format, too many spaces between '# and art-xxx' in file", self.filename
-                print
+                log.error("LINE: %s", line.rstrip())
+                log.error("Header Validation - invalid header format, too many spaces between '# and art-xxx' in file %s", self.filename)
+                log.error("")
             if self.header_format_error3.match(line):
-                print "LINE: ", line.rstrip()
-                print "ERROR: Header Validation - invalid header format, use at least one space between ': and value' in file", self.filename
-                print
+                log.error("LINE: %s", line.rstrip())
+                log.error("Header Validation - invalid header format, use at least one space between ': and value' in file %s", self.filename)
+                log.error("")
 
         for key in self.header:
             if 'type' not in self.header[key]:
-                print "ERROR: Header Validation - Invalid key:", key, "in file", self.filename
-                print
+                log.error("Header Validation - Invalid key: %s in file %s", key, self.filename)
+                log.error("")
                 continue
             if type(self.header[key]['value']) != self.header[key]['type']:
                 if not isinstance(self.header[key]['value'], type(None)):
-                    print "ERROR: Header Validation - value_type:", type(self.header[key]['value']), "not valid for key:", key, "expected value_type:", self.header[key]['type'], "in file", self.filename
-                    print
+                    log.error("Header Validation - value_type: %s not valid for key: %s, expected value_type: %s in file %s", type(self.header[key]['value']), key, self.header[key]['type'], self.filename)
+                    log.error("")
             if self.header[key]['constraint'] is not None and self.header[key]['value'] not in self.header[key]['constraint']:
                 if self.header[key]['value'] is None:
-                    print "ERROR: Header Validation - missing key:", key, "in file", self.filename
+                    log.error("Header Validation - missing key: %s in file %s", key, self.filename)
                 else:
-                    print "ERROR: Header Validation - value:", self.header[key]['value'], "for key:", key, "not in constraints:", self.header[key]['constraint'], "in file", self.filename
-                print
+                    log.error("Header Validation - value: %s for key: %s not in constraints: %s in file %s", self.header[key]['value'], key, self.header[key]['constraint'], self.filename)
+                log.error("")
 
         return 0
diff --git a/Tools/ART/python/ART/art_misc.py b/Tools/ART/python/ART/art_misc.py
index edb77fe5ebe1783f121b46d7b5a40e240254b2db..d81f3440599e4219e12ac519863f43bc10c3bc95 100644
--- a/Tools/ART/python/ART/art_misc.py
+++ b/Tools/ART/python/ART/art_misc.py
@@ -5,14 +5,40 @@
 __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
 
 import errno
+import logging
 import os
 import shlex
 import subprocess
+import sys
+
+MODULE = "art.misc"
+
+
+def set_log(kwargs):
+    """TBD."""
+    level = logging.DEBUG if kwargs['verbose'] else logging.WARN if kwargs['quiet'] else logging.INFO
+    log = logging.getLogger("art")
+    log.setLevel(level)
+
+    # create and attach new handler, disable propagation to root logger to avoid double messages
+    handler = logging.StreamHandler(sys.stdout)
+    format_string = "%(asctime)s %(name)15s.%(funcName)-15s %(levelname)8s %(message)s"
+    date_format_string = None
+    formatter = logging.Formatter(format_string, date_format_string)
+    handler.setFormatter(formatter)
+    log.addHandler(handler)
+    log.propagate = False
 
 
 def run_command(cmd, dir=None, shell=False, env=None):
-    """Run the given command locally and returns the output, err and exit_code."""
-    # print "Execute: " + cmd
+    """
+    Run the given command locally.
+
+    The command runs as separate subprocesses for every piped command.
+    Returns tuple of exit_code, output and err.
+    """
+    log = logging.getLogger(MODULE)
+    log.debug("Execute: %s", cmd)
     if "|" in cmd:
         cmd_parts = cmd.split('|')
     else:
@@ -33,39 +59,6 @@ def run_command(cmd, dir=None, shell=False, env=None):
     return exit_code, str(output), str(err)
 
 
-def exit_on_failure((exit_code, out, err)):
-    """Check exitcode and print statement and exit if needed."""
-    if exit_code == 0:
-        print err
-        return out
-
-    print "Error:", exit_code
-    print "StdOut:", out
-    print "StdErr:", err
-
-    print 'art-status: error'
-
-    exit(exit_code)
-
-
-def code_tbr((exit_code, out, err)):
-    """Check exitcode and print statement."""
-    if exit_code == 0:
-        print out
-        return exit_code
-
-    print "Error:", exit_code
-    print "StdOut:", out
-    print "StdErr:", err
-
-    return exit_code
-
-
-def redirect_tbr((exitcode, out, err)):
-    """Check exitcode."""
-    return exitcode
-
-
 def is_exe(fpath):
     """Return True if fpath is executable."""
     return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
diff --git a/Tools/ART/scripts/art-get-tar.sh b/Tools/ART/scripts/art-get-tar.sh
index 23de65640703a42cc83f6497ea197d2092071917..ec552a5ad34239fe87beda432299b7d1bf9cd9bb 100755
--- a/Tools/ART/scripts/art-get-tar.sh
+++ b/Tools/ART/scripts/art-get-tar.sh
@@ -2,14 +2,14 @@
 # Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
 #
 # NOTE do NOT run with /bin/bash -x as the output is too big for gitlab-ci
-# arguments:  PACKAGE INDEX EXTENSION NIGHTLY_RELEASE PROJECT PLATFORM NIGHTLY_TAG
+# arguments:  PACKAGE GRID_INDEX EXTENSION NIGHTLY_RELEASE PROJECT PLATFORM NIGHTLY_TAG
 #
 # author : Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>
 #
 # example: Tier0ChainTests 4 _EXT0 21.0 Athena x86_64-slc6-gcc62-opt 2017-07-24T2151
 
 if [ $# -ne 7 ]; then
-    echo 'Usage: art-get-tar.sh PACKAGE INDEX EXTENSION NIGHTLY_RELEASE PROJECT PLATFORM NIGHTLY_TAG'
+    echo 'Usage: art-get-tar.sh PACKAGE GRID_INDEX EXTENSION NIGHTLY_RELEASE PROJECT PLATFORM NIGHTLY_TAG'
     exit 1
 fi
 
@@ -19,7 +19,7 @@ ART_USER='artprod'
 
 PACKAGE=$1
 shift
-INDEX=$1
+GRID_INDEX=$1
 shift
 EXTENSION=$1
 shift
@@ -54,7 +54,7 @@ echo "Tar files in the Container: ${FILELIST}"
 CONTAINER=`rucio list-dids ${CONTAINER_LIST} --filter type=container | grep ${NIGHTLY_TAG} | sort -r | cut -d ' ' -f 2 | head -n 1`
 echo "Container: ${CONTAINER}"
 
-printf -v INDEX_FORMAT '_%06d.tar' ${INDEX}
+printf -v INDEX_FORMAT '_%06d.tar' ${GRID_INDEX}
 TAR_NAME=`rucio list-files --csv ${CONTAINER} | grep ${INDEX_FORMAT} | cut -d ',' -f 1`
 echo "Tar Name: ${TAR_NAME}"
 
diff --git a/Tools/ART/scripts/art-internal.py b/Tools/ART/scripts/art-internal.py
index 029ac3b1e559863b12e0ec70079bacf03cdafbfe..8c700482a7b965bf8a674ee24b0fab2bcd65c025 100755
--- a/Tools/ART/scripts/art-internal.py
+++ b/Tools/ART/scripts/art-internal.py
@@ -4,20 +4,19 @@
 ART-internal - ATLAS Release Tester (internal command).
 
 Usage:
-  art-internal.py job build   [-v]  <script_directory> <package> <job_type> <sequence_tag> <index> <out> <nightly_release> <project> <platform> <nightly_tag>
-  art-internal.py job grid    [-v --skip-setup]  <script_directory> <package> <job_type> <sequence_tag> <index_type> <index_or_name> <out> <nightly_release> <project> <platform> <nightly_tag>
-  art-internal.py task build  [-v]  <script_directory> <package> <job_type> <sequence_tag> <nightly_release> <project> <platform> <nightly_tag>
-  art-internal.py task grid   [-v --skip-setup]  <submit_directory> <script_directory> <package> <job_type> <sequence_tag> <nightly_release> <project> <platform> <nightly_tag>
+  art-internal.py job build   [-v -q]  <script_directory> <package> <job_type> <sequence_tag> <index> <out> <nightly_release> <project> <platform> <nightly_tag>
+  art-internal.py job grid    [-v -q --skip-setup]  <script_directory> <package> <job_type> <sequence_tag> <index_type> <index_or_name> <out> <nightly_release> <project> <platform> <nightly_tag>
 
 Options:
   --skip-setup      Do not run atlas setup or voms
   -h --help         Show this screen.
-  -v, --verbose     Show details.
+  -q --quiet        Show less information, only warnings and errors
+  -v --verbose      Show more information, debug level
   --version         Show version.
 
 Sub-commands:
-  job               Runs a single job, given a particular index
-  task              Runs a single task, consisting of given number of jobs
+  job               Run a single job, given a particular index
+  copy              Copy outputs to eos area
 
 Arguments:
   index_type        Type of index used (e.g. batch or single)
@@ -37,6 +36,7 @@ Arguments:
 
 __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
 
+import logging
 import os
 import sys
 
@@ -44,47 +44,45 @@ from ART.docopt_dispatch import dispatch
 
 from ART import ArtGrid, ArtBuild
 
+from ART.art_misc import set_log
+
+MODULE = "art.internal"
+
 
 @dispatch.on('job', 'build')
 def job_build(script_directory, package, job_type, sequence_tag, index, out, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD.
+    """Run a single job, given a particular index.
 
     Tests are called with the following parameters:
     SCRIPT_DIRECTORY, PACKAGE, TYPE, TEST_NAME
     """
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     exit(ArtBuild(art_directory, nightly_release, project, platform, nightly_tag, script_directory).job(package, job_type, sequence_tag, index, out))
 
 
 @dispatch.on('job', 'grid')
 def job_grid(script_directory, package, job_type, sequence_tag, index_type, index_or_name, out, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD.
+    """Run a single job, given a particular index.
 
     Tests are called with the following parameters:
     SCRIPT_DIRECTORY, PACKAGE, TYPE, TEST_NAME, NIGHTLY_RELEASE, PROJECT, PLATFORM, NIGHTLY_TAG
     """
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     skip_setup = kwargs['skip_setup']
     exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag, script_directory, skip_setup).job(package, job_type, sequence_tag, index_type, index_or_name, out))
 
 
-@dispatch.on('task', 'build')
-def task_build(script_directory, package, job_type, sequence_tag, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
-    art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
-    exit(ArtBuild(art_directory, nightly_release, project, platform, nightly_tag, script_directory).task(package, job_type, sequence_tag))
-
-
-@dispatch.on('task', 'grid')
-def task_grid(submit_directory, script_directory, package, job_type, sequence_tag, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
-    art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
-    skip_setup = kwargs['skip_setup']
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag, script_directory, skip_setup, submit_directory).task(package, job_type, sequence_tag))
-
-
 if __name__ == '__main__':
+    if sys.version_info < (2, 7, 0):
+        sys.stderr.write("You need python 2.7 or later to run this script\n")
+        exit(1)
+
     # NOTE: import should be here, to keep the order of the decorators (module first, art last and unused)
     from art import __version__
-    print "ART_PATH", os.path.dirname(os.path.realpath(sys.argv[0]))
+    logging.basicConfig()
+    log = logging.getLogger(MODULE)
+    log.setLevel(logging.INFO)
+    log.info("ART_PATH %s", os.path.dirname(os.path.realpath(sys.argv[0])))
     dispatch(__doc__, version=os.path.splitext(os.path.basename(__file__))[0] + ' ' + __version__)
diff --git a/Tools/ART/scripts/art-share.py b/Tools/ART/scripts/art-share.py
new file mode 100755
index 0000000000000000000000000000000000000000..77a97f00d1dc9b65cfb7a0974d9854f6a90b3e17
--- /dev/null
+++ b/Tools/ART/scripts/art-share.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+"""
+ART  - ATLAS Release Tester - Share.
+
+Usage:
+  art-share.py [options] <data_directory>
+
+Options:
+  -h --help               Show this screen
+  --version               Show version
+  --q --quiet             Show less information, only warnings and errors
+  --write                 Write to directory
+  -v --verbose            Show more information, debug level
+
+Arguments:
+  data_directory          directory to scan for shared files
+
+"""
+
+__author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
+
+import collections
+import hashlib
+import logging
+import os
+import sys
+
+try:
+    import scandir as scan
+except ImportError:
+    import os as scan
+
+from ART.docopt import docopt
+
+MODULE = "art.share"
+
+
+class ArtShare(object):
+    """Class for copying input files.
+
+    Files are copies to the .art area under their SHA1 sum. The orignal file is replaced
+    by a symbolic link. Any duplicates will result in the same SHA1 sum and thus just
+    be replaced by their symbiolic link. Removing a file results in removing a link.
+    If the .art directory has files without links pointing to them, these files are also
+    removed.
+    """
+
+    def __init__(self, data_directory, write):
+        """Constructor of ArtShare."""
+        log = logging.getLogger(MODULE)
+        self.data_directory = data_directory
+        self.write = write
+
+        self.art_directory = '.art'
+        self.extension = '.art'
+        self.count = collections.defaultdict(int)   # key is sha1
+
+        if not os.path.isdir(self.data_directory):
+            log.critical("data_directory does not exist: %s", self.data_directory)
+            sys.exit(1)
+
+        if self.write:
+            log.warning("NOTE - Changing File System")
+        else:
+            log.warning("NOT Changing File System, use --write to change the File System")
+
+        self.share()
+
+    def create_sha1sum(self, path):
+        """Calculate SHA1 from file on path."""
+        BUF_SIZE = 65536
+        sha1 = hashlib.sha1()
+        with open(path, 'rb') as f:
+            while True:
+                data = f.read(BUF_SIZE)
+                if not data:
+                    break
+                sha1.update(data)
+        return sha1.hexdigest()
+
+    def sha1sum(self, artpath):
+        """Retrieve SHA1 from artpath specification (in the filename)."""
+        f = os.path.basename(artpath)
+        return os.path.splitext(f)[0]
+
+    def share(self):
+        """Share the files by copying."""
+        log = logging.getLogger(MODULE)
+        art_root = os.path.join(self.data_directory, self.art_directory)
+        if not os.path.isdir(art_root):
+            log.info("NOTE - art_directory does not exist.")
+            log.info("       creating... %s", art_root)
+            if self.write:
+                os.makedirs(art_root)
+
+        if os.path.isdir(art_root):
+            for f in os.listdir(art_root):
+                sha1art = os.path.join(art_root, f)
+                if os.path.isfile(sha1art):
+                    sha1 = self.sha1sum(sha1art)
+                    self.count[sha1] = 0
+
+        i = 0
+        for root, dirs, files in scan.walk(self.data_directory):
+            for f in files:
+                if os.path.basename(root) == self.art_directory:
+                    continue
+
+                path = os.path.join(root, f)
+                i += 1
+                if os.path.islink(path):
+                    # link
+                    if not os.path.exists(path):
+                        log.warning("WARNING - Stale link/file, skipping")
+                        log.warning("          path: %s", path)
+                        continue
+                    sha1 = self.sha1sum(os.path.realpath(path))
+                    log.debug("Link %d path %s", i, path)
+                    log.debug("SHA1 %s", sha1)
+                    self.count[sha1] += 1
+                else:
+                    # file
+                    byte_size = os.path.getsize(path)
+                    if byte_size <= 0:
+                        log.warning("WARNING - zero sized file, skipping")
+                        log.warning("          path: %s", path)
+                        continue
+
+                    megabyte_size = byte_size / 1024 / 1024
+                    log.debug("File %d %s", i, path)
+                    log.debug("File size %d", megabyte_size)
+                    sha1 = self.create_sha1sum(path) if self.write or megabyte_size < 100 else "????????????????????????????????????????"
+                    log.debug("SHA1 %s", sha1)
+
+                    art_path = os.path.join(art_root, sha1 + self.extension)
+                    art_relpath = os.path.relpath(art_path, os.path.dirname(path))
+
+                    if sha1 not in self.count.keys():
+                        log.info("    Moving file from %s", path)
+                        log.info("                  to %s", art_path)
+                        if self.write:
+                            os.rename(path, art_path)
+                        self.count[sha1] = 0
+                    else:
+                        log.info("    Removing file from %s", path)
+                        if self.write:
+                            os.remove(path)
+
+                    log.info("    Creating link from %s", path)
+                    log.info("                    to %s", art_relpath)
+                    if self.write:
+                        os.symlink(art_relpath, path)
+                    self.count[sha1] += 1
+
+        for sha1, count in self.count.iteritems():
+            if count <= 0:
+                art_path = os.path.join(art_root, sha1 + self.extension)
+                log.info("    Removing file  %s", art_path)
+                if self.write:
+                    os.remove(art_path)
+
+
+if __name__ == '__main__':
+    if sys.version_info < (2, 7, 0):
+        sys.stderr.write("You need python 2.7 or later to run this script\n")
+        exit(1)
+
+    # NOTE: import should be here, to keep the order of the decorators (module first, art last and unused)
+    from art import __version__
+
+    logging.basicConfig()
+    log = logging.getLogger(MODULE)
+
+    arguments = docopt(__doc__, version=os.path.splitext(os.path.basename(__file__))[0] + ' ' + __version__)
+    level = logging.DEBUG if arguments['--verbose'] else logging.WARN if arguments['--quiet'] else logging.INFO
+    log.setLevel(level)
+    ArtShare(arguments['<data_directory>'], arguments['--write'])
diff --git a/Tools/ART/scripts/art-task-grid.sh b/Tools/ART/scripts/art-task-grid.sh
index 2c15ff91e7c0452447a7bc604218c9339c2eff00..ad7be6ed95e0a2b1d4e9657e1a35d5ce89c0b790 100755
--- a/Tools/ART/scripts/art-task-grid.sh
+++ b/Tools/ART/scripts/art-task-grid.sh
@@ -21,9 +21,10 @@ if [ $1 == "--skip-setup" ]; then
 fi
 TYPE_OPTION="batch %RNDM:0"
 PATHENA_OPTIONS="--destSE=CERN-PROD_SCRATCHDISK"
+PATHENA_TYPE_OPTIONS=""
 if [ $1 == "--test-name" ]; then
   TYPE_OPTION="single $2"
-  PATHENA_OPTIONS="--destSE=CERN-PROD_SCRATCHDISK --forceStaged"
+  PATHENA_TYPE_OPTIONS="--forceStaged"
   shift
   shift
 fi
@@ -39,18 +40,6 @@ if [ $1 == "--nFiles" ]; then
   shift
   shift
 fi
-NEVENTS=""
-if [ $1 == "--nEvents" ]; then
-  NEVENTS="--nEventsPerFile $2"
-  shift
-  shift
-fi
-FILELIST=""
-if [ $1 == "--fileList" ]; then
-  FILELIST="--fileList $2"
-  shift
-  shift
-fi
 SUBMIT_DIRECTORY=$1
 shift
 SCRIPT_DIRECTORY=$1
@@ -101,7 +90,7 @@ fi
 # NOTE: for art-internal.py the current dir can be used as it is copied there
 cd ${SUBMIT_DIRECTORY}/${PACKAGE}/run
 SUBCOMMAND="./art-internal.py job grid ${SCRIPT_DIRECTORY} ${PACKAGE} ${TYPE} ${SEQUENCE_TAG} ${TYPE_OPTION} %OUT.tar ${NIGHTLY_RELEASE_SHORT} ${PROJECT} ${PLATFORM} ${NIGHTLY_TAG}"
-CMD="pathena ${GRID_OPTIONS} ${PATHENA_OPTIONS} --noBuild --expertOnly_skipScout --trf \"${SUBCOMMAND}\" ${SPLIT} --outDS ${OUTFILE} --extOutFile art-job.json ${INDS} ${NFILES} ${NEVENTS} ${FILELIST}"
+CMD="pathena ${GRID_OPTIONS} ${PATHENA_OPTIONS} ${PATHENA_TYPE_OPTIONS} --noBuild --expertOnly_skipScout --trf \"${SUBCOMMAND}\" ${SPLIT} --outDS ${OUTFILE} --extOutFile art-job.json ${INDS} ${NFILES}"
 
 #--disableAutoRetry
 #--excludedSite=ANALY_TECHNION-HEP-CREAM
diff --git a/Tools/ART/scripts/art.py b/Tools/ART/scripts/art.py
index 9a0da14c933f88fded8f57ea55f36d1728e0a2d7..54d8d37a47c6fa3a5689c820863bbb41a6038431 100755
--- a/Tools/ART/scripts/art.py
+++ b/Tools/ART/scripts/art.py
@@ -4,38 +4,44 @@
 ART - ATLAS Release Tester.
 
 Usage:
-  art.py run             [-v --type=<T> --max-jobs=<N> --ci] <script_directory> <sequence_tag>
-  art.py grid            [-v --type=<T>] <script_directory> <sequence_tag>
-  art.py submit          [-v --type=<T>] <sequence_tag> <nightly_release> <project> <platform> <nightly_tag>
-  art.py validate        [-v] <script_directory>
-  art.py included        [-v --type=<T> --test-type=<TT>] <script_directory> [<nightly_release> <project> <platform>]
-  art.py compare grid    [-v --days=<D>] <nightly_release> <project> <platform> <nightly_tag> <package> <test_name> <file_name>...
-  art.py compare ref     [-v]  <file_name> <ref_file>
-  art.py download        [-v] <input_file>
-  art.py list grid       [-v --json --type=<T> --test-type=<TT>] <package> <nightly_release> <project> <platform> <nightly_tag>
-  art.py log grid        [-v] <package> <test_name> <nightly_release> <project> <platform> <nightly_tag>
-  art.py output grid     [-v] <package> <test_name> <file_name> <nightly_release> <project> <platform> <nightly_tag>
+  art.py run             [-v -q --type=<T> --max-jobs=<N> --ci] <script_directory> <sequence_tag>
+  art.py grid            [-v -q --type=<T> -n] <script_directory> <sequence_tag>
+  art.py submit          [-v -q --type=<T> -n] <sequence_tag> <nightly_release> <project> <platform> <nightly_tag> [<package>]
+  art.py copy            [-v -q --user=<user> --dst=<dir>] <sequence_tag> <nightly_release> <project> <platform> <nightly_tag> [<package>]
+  art.py validate        [-v -q] <script_directory>
+  art.py included        [-v -q --type=<T> --test-type=<TT>] <script_directory> [<nightly_release> <project> <platform>]
+  art.py compare grid    [-v -q --days=<D> --user=<user>] <nightly_release> <project> <platform> <nightly_tag> <package> <test_name> <file_name>...
+  art.py compare ref     [-v -q]  <file_name> <ref_file>
+  art.py download        [-v -q] <input_file>
+  art.py list grid       [-v -q --user=<user> --json --type=<T> --test-type=<TT>] <package> <nightly_release> <project> <platform> <nightly_tag>
+  art.py log grid        [-v -q --user=<user>] <package> <test_name> <nightly_release> <project> <platform> <nightly_tag>
+  art.py output grid     [-v -q --user=<user>] <package> <test_name> <nightly_release> <project> <platform> <nightly_tag>
 
 Options:
   --ci              Run Continuous Integration tests only (using env: AtlasBuildBranch)
   --days=<D>        Number of days ago to pick up reference for compare [default: 1]
+  --dst=<dir>       Destination directory for downloaded files
+  -h --help         Show this screen.
   --json            Output in json format
   --max-jobs=<N>    Maximum number of concurrent jobs to run [default: 0]
+  -n --no-action    No real submit will be done
+  -q --quiet        Show less information, only warnings and errors
+  --test-type=<TT>  Type of test (e.g. all, batch or single) [default: all]
   --type=<T>        Type of job (e.g. grid, build)
-  --test-type=<TT>  Type of test (e.g. all, batch or single)
-  -h --help         Show this screen.
-  -v, --verbose     Show details.
+  --user=<user>     User to use for RUCIO
+  -v --verbose      Show more information, debug level
   --version         Show version.
 
 Sub-commands:
   run               Run jobs from a package in a local build (needs release and grid setup)
   grid              Run jobs from a package on the grid (needs release and grid setup)
   submit            Submit nightly jobs to the grid (NOT for users)
+  copy              Copy outputs and logs from RUCIO
   validate          Check headers in tests
-  included          Shows list of files which will be included for art submit/art grid
+  included          Show list of files which will be included for art submit/art grid
   compare           Compare the output of a job
   download          Download a file from rucio
-  list              Lists the jobs of a package
+  list              List the jobs of a package
   log               Show the log of a job
   output            Get the output of a job
 
@@ -55,8 +61,9 @@ Arguments:
 """
 
 __author__ = "Tulay Cuhadar Donszelmann <tcuhadar@cern.ch>"
-__version__ = '0.5.4'
+__version__ = '0.6.5'
 
+import logging
 import os
 import sys
 
@@ -64,62 +71,77 @@ from ART.docopt_dispatch import dispatch
 
 from ART import ArtBase, ArtGrid, ArtBuild
 
+from ART.art_misc import set_log
 
 #
 # First list the double commands
 #
 
+
 @dispatch.on('compare', 'ref')
 def compare_ref(file_name, ref_file, **kwargs):
-    """TBD."""
+    """Compare the output of a job."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     exit(ArtBase(art_directory).compare_ref(file_name, ref_file))
 
 
 @dispatch.on('compare', 'grid')
 def compare_grid(package, test_name, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
+    """Compare the output of a job."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     days = int(kwargs['days'])
     file_names = kwargs['file_name']
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).compare(package, test_name, days, file_names))
+    user = kwargs['user']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).compare(package, test_name, days, file_names, user))
 
 
 @dispatch.on('list', 'grid')
 def list(package, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
+    """List the jobs of a package."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     job_type = 'grid' if kwargs['type'] is None else kwargs['type']
-    index_type = 'all' if kwargs['test_type'] is None else kwargs['test_type']
+    index_type = kwargs['test_type']
     json_format = kwargs['json']
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).list(package, job_type, index_type, json_format))
+    user = kwargs['user']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).list(package, job_type, index_type, json_format, user))
 
 
 @dispatch.on('log', 'grid')
 def log(package, test_name, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
+    """Show the log of a job."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).log(package, test_name))
+    user = kwargs['user']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).log(package, test_name, user))
 
 
 @dispatch.on('output', 'grid')
-def output(package, test_name, file_name, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
+def output(package, test_name, nightly_release, project, platform, nightly_tag, **kwargs):
+    """Get the output of a job."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).output(package, test_name, file_name))
+    user = kwargs['user']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).output(package, test_name, user))
 
 
 @dispatch.on('submit')
 def submit(sequence_tag, nightly_release, project, platform, nightly_tag, **kwargs):
-    """TBD."""
+    """Submit nightly jobs to the grid, NOT for users."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
-    job_type = 'grid' if kwargs['type'] is None else kwargs['type']
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).task_list(job_type, sequence_tag))
+    job_type = kwargs['type']
+    package = kwargs['package']
+    no_action = kwargs['no_action']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).task_list(job_type, sequence_tag, package, no_action))
 
 
 @dispatch.on('grid')
 def grid(script_directory, sequence_tag, **kwargs):
-    """TBD."""
+    """Run jobs from a package on the grid, needs release and grid setup."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     try:
         nightly_release = os.environ['AtlasBuildBranch']
@@ -127,15 +149,18 @@ def grid(script_directory, sequence_tag, **kwargs):
         platform = os.environ[project + '_PLATFORM']
         nightly_tag = os.environ['AtlasBuildStamp']
     except KeyError, e:
-        print "Environment variable not set", e
+        log.critical("Environment variable not set %s", e)
         sys.exit(1)
     art_type = 'grid' if kwargs['type'] is None else kwargs['type']
-    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag, script_directory, True).task_list(art_type, sequence_tag))
+    package = None
+    no_action = kwargs['no_action']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag, script_directory, True).task_list(art_type, sequence_tag, package, no_action))
 
 
 @dispatch.on('run')
 def run(script_directory, sequence_tag, **kwargs):
-    """TBD."""
+    """Run jobs from a package in a local build, needs release and grid setup."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     try:
         nightly_release = os.environ['AtlasBuildBranch']
@@ -143,37 +168,57 @@ def run(script_directory, sequence_tag, **kwargs):
         platform = os.environ[project + '_PLATFORM']
         nightly_tag = os.environ['AtlasBuildStamp']
     except KeyError, e:
-        print "Environment variable not set", e
+        log.critical("Environment variable not set %s", e)
         sys.exit(1)
     art_type = 'build' if kwargs['type'] is None else kwargs['type']
     exit(ArtBuild(art_directory, nightly_release, project, platform, nightly_tag, script_directory, max_jobs=int(kwargs['max_jobs']), ci=kwargs['ci']).task_list(art_type, sequence_tag))
 
 
+@dispatch.on('copy')
+def copy(sequence_tag, nightly_release, project, platform, nightly_tag, **kwargs):
+    """Copy outputs to eos area."""
+    set_log(kwargs)
+    art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
+    package = kwargs['package']
+    # NOTE: default depends on USER, not set it here but in ArtGrid.copy
+    dst = kwargs['dst']
+    user = kwargs['user']
+    exit(ArtGrid(art_directory, nightly_release, project, platform, nightly_tag).copy(sequence_tag, package, dst, user))
+
+
 @dispatch.on('validate')
 def validate(script_directory, **kwargs):
-    """TBD."""
+    """Check headers in tests."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     exit(ArtBase(art_directory).validate(script_directory))
 
 
 @dispatch.on('included')
 def included(script_directory, **kwargs):
-    """TBD."""
+    """Show list of files which will be included for art submit/art grid."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     nightly_release = os.environ['AtlasBuildBranch'] if kwargs['nightly_release'] is None else kwargs['nightly_release']
     project = os.environ['AtlasProject'] if kwargs['project'] is None else kwargs['project']
     platform = os.environ[project + '_PLATFORM'] if kwargs['platform'] is None else kwargs['platform']
     art_type = 'grid' if kwargs['type'] is None else kwargs['type']
-    index_type = 'all' if kwargs['test_type'] is None else kwargs['test_type']
+    index_type = kwargs['test_type']
     exit(ArtBase(art_directory).included(script_directory, art_type, index_type, nightly_release, project, platform))
 
 
 @dispatch.on('download')
 def download(input_file, **kwargs):
-    """TBD."""
+    """Download a file from rucio."""
+    set_log(kwargs)
     art_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
     exit(ArtBase(art_directory).download(input_file))
 
 
 if __name__ == '__main__':
+    if sys.version_info < (2, 7, 0):
+        sys.stderr.write("You need python 2.7 or later to run this script\n")
+        exit(1)
+
+    logging.basicConfig()
     dispatch(__doc__, version=os.path.splitext(os.path.basename(__file__))[0] + ' ' + __version__)