#!/usr/bin/env python

from __future__ import print_function
import argparse
import base64
from collections import OrderedDict
from collections import namedtuple
import errno
import json
import os
import sys
import time
import requests

class ProminenceJob(object):
    """
    PROMINENCE job class
    """

    # List of all attribute names
    attrs = ['image',
             'cmd',
             'args',
             'nodes',
             'cpus',
             'memory',
             'disk',
             'runtime',
             'env',
             'labels',
             'artifacts',
             'inputs',
             'output_files',
             'output_dirs',
             'type',
             'mpi_version',
             'constraints']

    # The key is the attribute name and the value is the JSON key
    attr_map = {'image':'image',
                'cmd':'cmd',
                'args':'args',
                'nodes':'nodes',
                'cpus':'cpus',
                'memory':'memory',
                'disk':'disk',
                'runtime':'runtime',
                'env':'env',
                'labels':'labels',
                'artifacts':'artifacts',
                'inputs':'inputs',
                'output_files':'outputFiles',
                'output_dirs':'outputDirs',
                'type':'type',
                'mpi_version':'mpiVersion',
                'constraints':'constraints'}

    def __init__(self, image=None, cmd=None, args=None, nodes=None, cpus=None, memory=None, disk=None, runtime=None, env=None, labels=None, artifacts=None, inputs=None, output_files=None, output_dirs=None, type=None, mpi_version=None, constraints=None):

        self._image = image
        self._cmd = cmd
        self._args = args
        self._nodes = nodes
        self._cpus = cpus
        self._memory = memory
        self._disk = disk
        self._runtime = runtime
        self._env = env
        self._labels = labels
        self._artifacts = artifacts
        self._inputs = inputs
        self._output_files = output_files
        self._output_dirs = output_dirs
        self._type = type
        self._mpi_version = mpi_version
        self._constraints = constraints

    @property
    def image(self):
        """
        Gets the container image
        """
        return self._image

    @image.setter
    def image(self, image):
        """
        Sets the container image
        """
        self._image = image

    @property
    def cmd(self):
        """
        Gets the command
        """
        return self._cmd

    @cmd.setter
    def cmd(self, cmd):
        """
        Sets the command
        """
        self._cmd = cmd

    @property
    def args(self):
        """
        Gets the args
        """
        return self._args

    @args.setter
    def args(self, args):
        """
        Sets the args
        """
        self._args = args

    @property
    def nodes(self):
        """
        Returns the number of nodes
        """
        return self._nodes

    @nodes.setter
    def nodes(self, nodes):
        """
        Sets the number of nodes
        """
        self._nodes = nodes

    @property
    def cpus(self):
        """
        Gets the number of CPUs
        """
        return self._cpus

    @cpus.setter
    def cpus(self, cpus):
        """
        Sets the number of CPUs
        """
        self._cpus = cpus

    @property
    def memory(self):
        """
        Returns the memory in GB
        """
        return self._memory

    @memory.setter
    def memory(self, memory):
        """
        Sets the memory in GB
        """
        self._memory = memory

    @property
    def disk(self):
        """
        Returns the disk size in GB
        """
        return self._disk

    @disk.setter
    def disk(self, disk):
        """
        Sets the disk size in GB
        """
        self._disk = disk

    @property
    def runtime(self):
        """
        Returns the maximum runtime in mins
        """
        return self._runtime

    @runtime.setter
    def runtime(self, runtime):
        """
        Sets the maximum runtime in mins
        """
        self._runtime = runtime

    @property
    def env(self):
        """
        Returns the list of environment variables to be set in the container
        """
        return self._env

    @env.setter
    def env(self, env):
        """
        Sets the list of environment variables to be set in the container
        """
        self._env = env

    @property
    def labels(self):
        """
        Returns the list of labels associated with the job
        """
        return self._labels

    @labels.setter
    def labels(self, labels):
        """
        Sets the list of labels associated with the job
        """
        self._labels = labels

    @property
    def artifacts(self):
        """
        Returns the list of artifacts to be downloaded before the job starts
        """
        return self._artifacts

    @artifacts.setter
    def artifacts(self, artifacts):
        """
        Sets the list of artifacts to be downloaded before the job starts
        """
        self._artifacts = artifacts

    @property
    def inputs(self):
        """
        Returns the input files
        """
        return self._inputs

    @inputs.setter
    def inputs(self, inputs):
        """
        Sets the list of inputs
        """
        self._inputs = inputs

    @property
    def output_files(self):
        """
        Returns the list of output files to be uploaded to cloud storage
        """
        return self._output_files

    @output_files.setter
    def output_files(self, output_files):
        """
        Sets the list of output files to be uploaded to cloud storage
        """
        self._output_files = output_files

    @property
    def output_dirs(self):
        """
        Returns the list of output directories to be uploaded to cloud storage
        """
        return self._output_dirs

    @output_dirs.setter
    def output_dirs(self, output_dirs):
        """
        Sets the list of output directories to be uploaded to cloud storage
        """
        self._output_dirs = output_dirs

    @property
    def type(self):
        """
        Returns the type of job
        """
        return self._type

    @type.setter
    def type(self, type):
        """
        Sets the type of job ('basic' or 'mpi')
        """
        self._type = type

    @property
    def mpi_version(self):
        """
        Returns the MPI version
        """
        return self._mpi_version

    @mpi_version.setter
    def mpi_version(self, mpi_version):
        """
        Sets the MPI version
        """
        self._mpi_version = mpi_version

    @property
    def constraints(self):
        """
        Returns the placement constraints
        """
        return self._constraints

    @constraints.setter
    def constraints(self, constraints):
        """
        Sets the placement constraints
        """
        self._constraints = constraints

    def to_json(self):
        """
        Returns the job as JSON
        """
        data = {}
        for attr in self.attrs:
            value = getattr(self, attr, None)
            if value is not None:
                data[self.attr_map[attr]] = value
        return data

    def from_json(self, data):
        """
        Initializes job using JSON representation
        """
        for attr in self.attrs:
            attr_json = self.attr_map[attr]
            if attr_json in data:
                setattr(self, attr, data[attr_json])

class ProminenceClient(object):
    """
    PROMINENCE client class
    """

    # Named tuple containing a return code & data object
    Response = namedtuple("Response", ["return_code", "data"])

    def __init__(self, url=None, token=None):
        self._url = url
        self._timeout = 10
        self._headers = {"Authorization":"Bearer %s" % token}

    def list(self, completed, all, num, constraint):
        """
        List running/idle jobs or completed jobs
        """

        params = {}

        if completed:
            params['completed'] = 'true'

        if num:
            params['num'] = num

        if all:
            params['all'] = 'true'

        if constraint:
            params['constraint'] = constraint

        try:
            response = requests.get(self._url + '/jobs', params=params, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})

        if response.status_code == 200:
            return self.Response(return_code=0, data=response.json())
        elif response.status_code < 500:
            if 'error' in response.json():
                return self.Response(return_code=1, data={'error': '%s' % response.json()['error']})
        return self.Response(return_code=1, data={'error': 'unknown'})

    def create(self, job):
        """
        Create a job
        """
        data = job.to_json()
        try:
            response = requests.post(self._url + '/jobs', json=data, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})
        if response.status_code == 201:
            if 'id' in response.json():
                return self.Response(return_code=0, data={'id': response.json()['id']})
        return self.Response(return_code=1, data={'error': 'unknown'})

    def delete(self, job_id):
        """
        Delete the specified job
        """
        try:
            response = requests.delete(self._url + '/jobs/%d' % job_id, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})

        if response.status_code == 200:
            return self.Response(return_code=0, data={})
        else:
            if 'error' in response.json():
                return self.Response(return_code=1, data={'error': '%s' % response.json()['error']})
        return self.Response(return_code=1, data={'error': 'unknown'})

    def describe(self, job_id, completed=False):
        """
        Describe a specific job
        """
        if completed:
            completed = 'true'
        else:
            completed = 'false'
        params = {'completed':completed, 'num':1}

        try:
            response = requests.get(self._url + '/jobs/%d' % job_id, params=params, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})

        if response.status_code == 200:
            return self.Response(return_code=0, data=response.json())
        elif response.status_code < 500:
            if 'error' in response.json():
                return self.Response(return_code=1, data={'error': '%s' % response.json()['error']})
        return self.Response(return_code=1, data={'error': 'unknown'})

    def stdout(self, job_id):
        """
        Get standard output from a job
        """

        try:
            response = requests.get(self._url + '/jobs/%d/0/stdout' % job_id, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})

        if response.status_code == 200:
            return self.Response(return_code=0, data=response.content)
        else:
            if 'error' in response.json():
                return self.Response(return_code=1, data={'error': '%s' % response.json()['error']})
        return self.Response(return_code=1, data={'error': 'unknown'})

    def stderr(self, job_id):
        """
        Get standard error from a job
        """

        try:
            response = requests.get(self._url + '/jobs/%d/0/stderr' % job_id, timeout=self._timeout, headers=self._headers)
        except requests.exceptions.RequestException:
            return self.Response(return_code=1, data={'error': 'cannot connect to PROMINENCE server'})

        if response.status_code == 200:
            return self.Response(return_code=0, data=response.content)
        else:
            if 'error' in response.json():
                return self.Response(return_code=1, data={'error': '%s' % response.json()['error']})
        return self.Response(return_code=1, data={'error': 'unknown'})

def print_json(content, transform=False, detail=False):
    """
    Print json in a nice way
    """
    if transform:
        content = transform_job_list(content, detail)
    print(json.dumps(content, indent=2))

def image_name(name):
    """
    Extract container image name
    """
    if 'http' in name:
        name = os.path.basename(name)
        name = name[:name.find('?')]
    return name

def list_jobs(jobs):
    """
    Print list of jobs
    """

    # Firstly determine column widths
    width_id = 2
    width_created = 19
    width_status = 6
    width_container = 5
    width_cmd = 3

    for job in jobs:
        my_cmd = ''
        if 'cmd' in job:
            my_cmd = os.path.basename(job['cmd'])

        width_id_current = len(str(job['id']))
        width_status_current = len(job['status'])
        width_container_current = len(image_name(job['image']))
        width_cmd_current = len((my_cmd))

        if width_id_current > width_id:
            width_id = width_id_current
        if width_status_current > width_status:
            width_status = width_status_current
        if width_container_current > width_container:
            width_container = width_container_current
        if width_cmd_current > width_cmd:
            width_cmd = width_cmd_current

    # Print headings
    print('%s   %s   %s   %s   %s %s' % ('ID'.ljust(width_id),
                                         'CREATED'.ljust(width_created),
                                         'STATUS'.ljust(width_status),
                                         'IMAGE'.ljust(width_container),
                                         'CMD'.ljust(width_cmd),
                                         'ARGS'))

    # Print jobs
    for job in jobs:
        my_cmd = ''
        my_args = ''
        if 'cmd' in job:
            my_cmd = os.path.basename(job['cmd'])
        if 'args' in job:
            my_args = job['args']
        print('%s   %s   %s   %s   %s %s' % (str(job['id']).ljust(width_id),
                                             job['events']['creation'].ljust(width_created),
                                             job['status'].ljust(width_status),
                                             image_name(job['image']).ljust(width_container),
                                             my_cmd.ljust(width_cmd),
                                             my_args))

def transform_job(job, detail):
    """
    Transform a job into the required format
    """
    job_t = OrderedDict()
    job_t['id'] = job['id']
    job_t['status'] = job['status']

    if detail and 'statusReason' in job:
        job_t['statusReason'] = job['statusReason']

    job_t['image'] = job['image']
    if 'cmd' in job:
        job_t['cmd'] = job['cmd']
    if 'args' in job:
        job_t['args'] = job['args']

    events = OrderedDict()
    if 'events' in job:
        if 'creation' in job['events']:
            events['creation'] = job['events']['creation']

    if detail:
        job_t['cpus'] = job['cpus']
        job_t['memory'] = job['memory']
        job_t['nodes'] = job['nodes']
        job_t['disk'] = job['disk']
        job_t['runtime'] = job['runtime']
        if 'env' in job:
            job_t['env'] = job['env']
        if 'labels' in job:
            job_t['labels'] = job['labels']
        if 'artifacts' in job:
            job_t['artifacts'] = job['artifacts']
        if 'inputFiles' in job:
            job_t['inputFiles'] = job['inputFiles']
        if 'outputFiles' in job:
            job_t['outputFiles'] = job['outputFiles']

        if 'containerCreationStart' in job['events']:
            events['containerCreationStart'] = job['events']['containerCreationStart']
        if 'executionStart' in job['events']:
            events['executionStart'] = job['events']['executionStart']
        if 'completionTime' in job['events']:
            events['completionTime'] = job['events']['completionTime']
    job_t['events'] = events
    return job_t

def transform_job_list(result, detail):
    """
    Transform a job list into the required format ordered by id
    """
    jobs = [transform_job(job, detail) for job in result]
    return sorted(jobs, key=lambda k: int(k['id']))

def command_login(args):
    """
    Login to IAM
    """
    data = {}
    data['scope'] = 'openid profile email'
    data['client_id'] = os.environ['PROMINENCE_IAM_CLIENT_ID']

    try:
        request = requests.post(os.environ['PROMINENCE_IAM_URL']+'/devicecode', data=data, timeout=HTTP_TIMEOUT, auth=(os.environ['PROMINENCE_IAM_CLIENT_ID'], os.environ['PROMINENCE_IAM_CLIENT_SECRET']), allow_redirects=True)
    except requests.exceptions.RequestException:
        print('Error: Cannot connect to IAM server')
        exit(1)

    device_code_response = request.json()

    print('To login, use a web browser to open the page %s and enter the code %s when requested' % (device_code_response['verification_uri'], device_code_response['user_code']))

    data = {}
    data['grant_type'] = 'urn:ietf:params:oauth:grant-type:device_code'
    data['device_code'] = device_code_response['device_code']

    # Wait for the user to authenticate
    current_time = time.time()
    authenticated = False
    while time.time() < current_time + int(device_code_response['expires_in']) and not authenticated:
        time.sleep(5)
        try:
            request = requests.post(os.environ['PROMINENCE_IAM_URL']+'/token', data=data, timeout=HTTP_TIMEOUT, auth=(os.environ['PROMINENCE_IAM_CLIENT_ID'], os.environ['PROMINENCE_IAM_CLIENT_SECRET']), allow_redirects=True)
        except requests.exceptions.RequestException:
            print('Error: Cannot connect to IAM server')
            exit(1)
        if request.status_code == 200:
            authenticated = True
            with open(os.path.expanduser('~/.prominence'), 'w') as token_file:
                json.dump(request.json(), token_file)
            os.chmod(os.path.expanduser('~/.prominence'), 384)
            print('Authentication successful')
            exit(0)

    if not authenticated:
        print('Error: Authentication failed')
        exit(1)

def get_token():
    """
    Load saved token
    """
    if os.path.isfile(os.path.expanduser('~/.prominence')):
        with open(os.path.expanduser('~/.prominence')) as json_data:
            data = json.load(json_data)
            if 'access_token' in data:
                return data['access_token']
    return None

def command_list(args):
    """
    List running/idle jobs or completed jobs
    """
    completed = False
    if args.completed:
        completed = True
    all = False
    if args.all:
        all = True
    num = None
    if args.num:
        num = args.num
    constraint = None
    if args.constraint:
        constraint = args.constraint

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.list(completed, all, num, constraint)
    if response.return_code == 0:
        list_jobs(transform_job_list(response.data, False))
        exit(0)
    else:
        print(response.data['error'])
        exit(1)

def command_describe(args):
    """
    Describe specific job
    """
    completed = False
    if args.completed:
        completed = True

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.describe(args.id, completed)
    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print_json(response.data)
    exit(0)

def command_download(args):
    """
    Download output files
    """
    client = ProminenceClient(url=URL, token=TOKEN)

    if args.constraint:
        response = client.list(False, True, 0, args.constraint)
    else:
        if not args.id:
            print('Error: A job id must be given if a constraint if not specified')
            exit(1)
        response = client.describe(args.id, True)

    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)

    jobs = response.data
    for job in jobs:
        if 'outputFiles' in job and job['status'] == 'completed':
            # Create per-job directory if necessary & set path for files
            path = './'
            if args.dir:
                path = './%d/' % job['id']
                try:
                    os.mkdir(path)
                except OSError as exc:
                    if exc.errno != errno.EEXIST:
                        print('WARNING: skipping job %d as job directory cannot be created' % job['id'])
                        continue
                    else:
                        pass

            for pair in job['outputFiles']:
                file_name = os.path.basename(pair['name'])
                url = pair['url']

                if os.path.isfile(path + file_name) and not args.force:
                    print('WARNING: skipping "%s" from job %d as file already exists and force option not specified' % (file_name, job['id']))
                    continue

                response = requests.get(url, stream=True)
                total_length = response.headers.get('content-length')

                if response.status_code != 200:
                    print('WARNING: skipping "%s" from job %d as it does not exist' % (file_name, job['id']))
                    continue

                with open(path + file_name, 'wb') as file_download:
                    print('Downloading file "%s" from job %d' % (file_name, job['id']))
                    if total_length is None:
                        file_download.write(response.content)
                    else:
                        downloaded = 0
                        total_length = int(total_length)
                        for data in response.iter_content(chunk_size=4096):
                            downloaded += len(data)
                            file_download.write(data)
                            done = int(50 * downloaded / total_length)
                            sys.stdout.write("\r[%s%s]" % ('=' * done, ' ' * (50-done)))
                            sys.stdout.flush()
                print('')

def command_delete(args):
    """
    Delete a job
    """
    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.delete(args.id)
    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print('Success')
    exit(0)

def command_stdout(args):
    """
    Get standard output for a specific job
    """
    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.stdout(args.id)
    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print(response.data)
    exit(0)

def command_stderr(args):
    """
    Get standard error for a specific job
    """
    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.stderr(args.id)
    if response.return_code != 0:
        print('Error: %s' % response.data['error'])
        exit(1)
    print(response.data)
    exit(0)

def command_create(args):
    """
    Create a job from a JSON file
    """
    job = ProminenceJob()

    try:
        with open(args.file) as json_file:
            data = json.load(json_file)
    except IOError as err:
        print('Error: %s' % err)
        exit(1)
    job.from_json(data)

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.create(job)
    if response.return_code == 0:
        if 'id' in response.data:
            print('Job created with id %d' % response.data['id'])
        exit(0)
    else:
        if 'error' in response.data:
            print('Error: %s' % response.data['error'])
    exit(1)

def command_run(args):
    """
    Run a job
    """
    job = ProminenceJob()

    job.image = args.image
    job.memory = args.memory
    job.cpus = args.cpus
    job.nodes = args.nodes
    job.disk = args.disk
    #job.instances = args.instances

    # Parallelism
    #if  args.parallelism:
    #    job.parallelism = args.parallelism

    # Maximum runtime
    if args.runtime:
        job.runtime = args.runtime

    # Job type
    if args.mpi:
        job.type = 'mpi'
    else:
        job.type = 'basic'

    # Force MPI if more than one node has been requested
    if job.nodes > 1:
        job.type = 'mpi'

    if args.mpiversion:
        job.mpi_version = args.mpiversion

    # Extract command and arguments from command string
    if args.command:
        if ' ' in args.command:
            job.cmd = args.command.split(" ", 1)[0]
            job.args = args.command.split(" ", 1)[1]
        else:
            job.cmd = args.command

    # Output files
    if args.outputfile:
        job.output_files = args.outputfile

    if args.outputdir:
        job.output_dirs = args.outputdir

    # Files to be fetched
    if args.artifact:
        job.artifacts = args.artifact

    # Files to be uploaded
    if args.inputfile:
        inputs = []
        for filename in args.inputfile:
            if os.path.isfile(filename):
                if os.path.getsize(filename) < 1000000:
                    with open(filename, 'rb') as input_file:
                        inputs.append({'filename':filename, 'content':base64.b64encode(input_file.read())})
                else:
                    print('Error: Input file size too large')
                    exit(1)
        job.inputs = inputs

    # Environment variables
    if args.env:
        job.env = args.env

    # Metadata
    if args.label:
        job.labels = args.label

    # Constraints
    if args.constraints:
        job.constraints = json.loads(args.constraints)

    if args.dryrun:
        print_json(job.to_json())
        exit(0)

    client = ProminenceClient(url=URL, token=TOKEN)
    response = client.create(job)
    if response.return_code == 0:
        if 'id' in response.data:
            print('Job created with id %d' % response.data['id'])
        exit(0)
    else:
        if 'error' in response.data:
            print('Error: %s' % response.data['error'])
    exit(1)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Prominence - \
                                     run jobs in containers across clouds')
    subparsers = parser.add_subparsers(help='sub-command help')

    # Create the parser for the "login" command
    parser_login = subparsers.add_parser('login', help='Login')
    parser_login.set_defaults(func=command_login)

    # Create the parser for the "create" command
    parser_create = subparsers.add_parser('create', help='Create a job from a JSON file')
    parser_create.add_argument('file', help='JSON file')
    parser_create.set_defaults(func=command_create)

    # Create the parser for the "run" command
    parser_run = subparsers.add_parser('run', help='Run a job')
    parser_run.add_argument("--memory", dest="memory", default=1, type=int,
                               help="Memory in GB per node")
    parser_run.add_argument("--cpus", dest="cpus", default=1, type=int,
                               help="Cores per node")
    parser_run.add_argument("--nodes", dest="nodes", default=1, type=int,
                               help="Number of nodes. MPI will be assumed if more \
                                     than one node is specified.")
    parser_run.add_argument("--instances", dest="instances", default=1, type=int,
                               help="Number of instances.")
    parser_run.add_argument("--parallelism", dest="parallelism", type=int,
                               help="Number of concurrent running instances.")
    parser_run.add_argument("--disk", dest="disk", default=10, type=int,
                               help="Size of disk containing the job's scratch directory. For \
                                     multi-node jobs it will be shared across each of the nodes. \
                                     By default a 10 GB disk will be used.")
    parser_run.add_argument("--runtime", dest="runtime", type=int,
                               help="Maximum runtime in minutes")
    parser_run.add_argument("--mpi", dest="mpi", default=False, action='store_true',
                               help="Specify that this is an MPI job")
    parser_run.add_argument("--mpi-version", dest="mpiversion",
                               choices=['1.10.7', '2.1.1', '3.0.2', '3.1.0'],
                               help="MPI version. The default is 2.1.1")
    parser_run.add_argument("--artifact", dest="artifact", action='append',
                               help="A URL to be transferred to the job. Archives will be \
                                     automatically unpacked/extracted. \
                                     This option can be specified multiple times.")
    parser_run.add_argument("--input", dest="inputfile", action='append',
                               help="Full path to a file on the current host to be \
                                     uploaded and made available to the job. This option \
                                     can be specified multiple times to set multiple output files.")
    parser_run.add_argument("--output", dest="outputfile", action='append',
                               help="An output file to be copied to cloud storage. This option \
                                     can be specified multiple times to set multiple output files.")
    parser_run.add_argument("--output-dir", dest="outputdir", action='append',
                               help="Output directory")
    parser_run.add_argument("--env", dest="env", action='append',
                               help="Specify environment variables in the form name=value. \
                                     This option can be specified multiple times to set \
                                     multiple environment variables.")
    parser_run.add_argument("--label", dest="label", action='append',
                               help="Set metadata in the form key=value. This option can \
                                     be specified multiple times to set multiple labels.")
    parser_run.add_argument("--constraints", dest="constraints",
                               help="Specify constraints")
    parser_run.add_argument("--dry-run", dest="dryrun", default=False, action='store_true',
                               help="Print json to stdout but don't actually create job")
    parser_run.add_argument('image', help='Container image')
    parser_run.add_argument('command', nargs='?',
                               help='Command to run in the container. If you need to specify \
                                     arguments, put the combined command and arguments inside quotes.')
    parser_run.set_defaults(func=command_run)

    # Create the parser for the "list" command
    parser_list = subparsers.add_parser('list', help='List jobs')
    parser_list.add_argument("--completed", dest="completed", default=False,
                             help="List completed jobs", action='store_true')
    parser_list.add_argument("--num", dest="num", default=1, type=int,
                             help="Number of completed jobs to return")
    parser_list.add_argument("--constraint", dest="constraint", action='append',
                             help="Constraint of the form key=value")
    parser_list.add_argument("--all", dest="all", default=False,
                             help="List jobs in all states", action='store_true')
    parser_list.set_defaults(func=command_list)

    # Create the parser for the "describe" command
    parser_describe = subparsers.add_parser('describe', help='Describe a job')
    parser_describe.add_argument('id', help='Job id', type=int)
    parser_describe.add_argument("--completed", dest="completed", default=False,
                            help="Describe a job in the completed state", action='store_true')
    parser_describe.set_defaults(func=command_describe)

    # Create the parser for the "delete" command
    parser_delete = subparsers.add_parser('delete', help='Delete a job')
    parser_delete.add_argument('id', help='Job id', type=int)
    parser_delete.set_defaults(func=command_delete)

    # Create the parser for the "download" command
    parser_download = subparsers.add_parser('download', help='Download output files from a job')
    parser_download.add_argument("--constraint", dest="constraint", action='append',
                                 help="Constraint of the form key=value")
    parser_download.add_argument('--force', dest='force', default=False,
                                 help='Force overwrite of existing file', action='store_true')
    parser_download.add_argument("--dir", dest="dir", default = False,
                                 help="Save output files in a directory named by the job id", action='store_true')
    parser_download.add_argument('id', help='Job id', type=int, nargs='?')
    parser_download.set_defaults(func=command_download)

    # Create the parser for the "stdout" command
    parser_stdout = subparsers.add_parser('stdout', help='Get standard output from a completed job')
    parser_stdout.add_argument('id', help='Job id', type=int)
    parser_stdout.set_defaults(func=command_stdout)

    # Create the parser for the "stderr" command
    parser_stderr = subparsers.add_parser('stderr', help='Get standard error from a completed job')
    parser_stderr.add_argument('id', help='Job id', type=int)
    parser_stderr.set_defaults(func=command_stderr)

    # Print help if necessary
    if len(sys.argv) < 2:
        parser.print_help(sys.stderr)
        exit(1)

    # Parse the arguments & run the required function if necessary
    args = parser.parse_args()

    # Authentication
    if 'PROMINENCE_IAM_URL' not in os.environ:
        print('Error: environment variable PROMINENCE_IAM_URL is not set')
        exit(1)
    if 'PROMINENCE_IAM_CLIENT_ID' not in os.environ:
        print('Error: environment variable PROMINENCE_IAM_CLIENT_ID is not set')
        exit(1)
    if 'PROMINENCE_IAM_CLIENT_SECRET' not in os.environ:
        print('Error: environment variable PROMINENCE_IAM_CLIENT_SECRET is not set')
        exit(1)

    # Get existing token if possible when necessary
    if sys.argv[1] != 'login':
        token = get_token()
        if token:
            HEADERS = {"Authorization":"Bearer %s" % token}
            TOKEN = token
        else:
            print('Error: Authentication required')
            exit(1)

    # Get URL
    if 'PROMINENCE_URL' in os.environ:
        URL = os.environ['PROMINENCE_URL']
    else:
        print('Error: Environment variable PROMINENCE_URL is not set')
        exit(1)

    HTTP_TIMEOUT = 10

    # Run
    args.func(args)
