Ejemplo n.º 1
0
class AWSBstatCommand:
    """awsbstat command."""

    __JOB_CONVERTERS = {"SIMPLE": JobConverter(), "ARRAY": ArrayJobConverter(), "MNP": MNPJobConverter()}

    def __init__(self, log, boto3_factory):
        """
        Initialize the object.

        :param log: log
        :param boto3_factory: an initialized Boto3ClientFactory object
        """
        self.log = log
        mapping = collections.OrderedDict(
            [
                ("jobId", "id"),
                ("jobName", "name"),
                ("createdAt", "creation_time"),
                ("startedAt", "start_time"),
                ("stoppedAt", "stop_time"),
                ("status", "status"),
                ("statusReason", "status_reason"),
                ("jobDefinition", "job_definition"),
                ("jobQueue", "queue"),
                ("command", "command"),
                ("exitCode", "exit_code"),
                ("reason", "reason"),
                ("vcpus", "vcpus"),
                ("memory[MB]", "memory"),
                ("nodes", "nodes"),
                ("logStream", "log_stream"),
                ("log", "log_stream_url"),
                ("s3FolderUrl", "s3_folder_url"),
            ]
        )
        self.output = Output(mapping=mapping)
        self.boto3_factory = boto3_factory
        self.batch_client = boto3_factory.get_client("batch")

    def run(self, job_status, expand_children, job_queue=None, job_ids=None, show_details=False):
        """Print list of jobs, by filtering by queue or by ids."""
        if job_ids:
            self.__populate_output_by_job_ids(job_ids, show_details or len(job_ids) == 1, include_parents=True)
            # explicitly asking for job details,
            # or asking for a single simple job (the output is not a list of jobs)
            details_required = show_details or (len(job_ids) == 1 and self.output.length() == 1)
        elif job_queue:
            self.__populate_output_by_queue(job_queue, job_status, expand_children, show_details)
            details_required = show_details
        else:
            fail("Error listing jobs from AWS Batch. job_ids or job_queue must be defined")

        sort_keys_function = self.__sort_by_status_startedat_jobid() if not job_ids else self.__sort_by_key(job_ids)
        if details_required:
            self.output.show(sort_keys_function=sort_keys_function)
        else:
            self.output.show_table(
                keys=["jobId", "jobName", "status", "startedAt", "stoppedAt", "exitCode"],
                sort_keys_function=sort_keys_function,
            )

    @staticmethod
    def __sort_by_key(ordered_keys):  # noqa: D202
        """
        Build a function to sort the output by key.

        :param ordered_keys: list containing the sorted keys.
        :return: a function to be used as key argument of the sorted function.
        """

        def _sort_by_key(item):
            job_id = item.id
            try:
                # in case the parent id was provided as input, sort children based on parent id position
                parent_id = re.findall(r"[\w-]+", job_id)[0]
                job_position = ordered_keys.index(parent_id)

            except ValueError:
                # in case the child id was provided as input, use its position in the list
                job_position = ordered_keys.index(job_id)

            return (
                # sort by id according to the order in the keys_order list
                job_position,
                # sort by full id (needed to have parent before children)
                job_id,
            )

        return _sort_by_key

    @staticmethod
    def __sort_by_status_startedat_jobid():
        """
        Build a function to sort the output by (status, startedAt, jobId).

        :return: a function to be used as key argument of the sorted function.
        """
        return lambda item: (
            # sort by status. Status order is defined by AWS_BATCH_JOB_STATUS.
            AWS_BATCH_JOB_STATUS.index(item.status),
            # sort by startedAt column.
            item.start_time,
            # sort by jobId column.
            item.id,
        )

    def __populate_output_by_job_ids(self, job_ids, details, include_parents=False):
        """
        Add Job item or jobs array children to the output.

        :param job_ids: job ids or ARNs
        :param details: ask for job details
        """
        try:
            if job_ids:
                self.log.info("Describing jobs (%s), details (%s)" % (job_ids, details))
                parent_jobs = []
                jobs_with_children = []
                jobs = self.__chunked_describe_jobs(job_ids)
                for job in jobs:
                    # always add parent job
                    if include_parents or get_job_type(job) == "SIMPLE":
                        parent_jobs.append(job)
                    if is_job_array(job):
                        jobs_with_children.append((job["jobId"], ":", job["arrayProperties"]["size"]))
                    elif is_mnp_job(job):
                        jobs_with_children.append((job["jobId"], "#", job["nodeProperties"]["numNodes"]))

                # add parent jobs to the output
                self.__add_jobs(parent_jobs)

                # create output items for jobs' children
                self.__populate_output_by_parent_ids(jobs_with_children)
        except Exception as e:
            fail("Error describing jobs from AWS Batch. Failed with exception: %s" % e)

    def __populate_output_by_parent_ids(self, parent_jobs):
        """
        Add jobs children to the output.

        :param parent_jobs: list of triplets (job_id, job_id_separator, job_size)
        """
        try:
            expanded_job_ids = []
            for parent_job in parent_jobs:
                expanded_job_ids.extend(
                    [
                        "{JOB_ID}{SEPARATOR}{INDEX}".format(JOB_ID=parent_job[0], SEPARATOR=parent_job[1], INDEX=i)
                        for i in range(0, parent_job[2])
                    ]
                )

            if expanded_job_ids:
                jobs = self.__chunked_describe_jobs(expanded_job_ids)

                # forcing details to be False since already retrieved.
                self.__add_jobs(jobs)
        except Exception as e:
            fail("Error listing job children. Failed with exception: %s" % e)

    def __chunked_describe_jobs(self, job_ids):
        """
        Submit calls to describe_jobs in batches of 100 elements each.

        describe_jobs API call has a hard limit on the number of job that can be
        retrieved with a single call. In case job_ids has more than 100 items, this function
        distributes the describe_jobs call across multiple requests.

        :param job_ids: list of ids for the jobs to describe.
        :return: list of described jobs.
        """
        jobs = []
        for index in range(0, len(job_ids), 100):
            jobs_chunk = job_ids[index : index + 100]  # noqa: E203
            jobs.extend(self.batch_client.describe_jobs(jobs=jobs_chunk)["jobs"])
        return jobs

    def __add_jobs(self, jobs, details=False):
        """
        Get job info from AWS Batch and add to the output.

        :param jobs: list of jobs items (output of the list_jobs function)
        :param details: ask for job details
        """
        try:
            if jobs:
                self.log.debug("Adding jobs to the output (%s)" % jobs)
                if details:
                    self.log.info("Asking for jobs details")
                    jobs_to_show = self.__chunked_describe_jobs([job["jobId"] for job in jobs])
                else:
                    jobs_to_show = jobs

                for job in jobs_to_show:
                    self.log.debug("Adding job to the output (%s)", job)

                    job_converter = self.__JOB_CONVERTERS[get_job_type(job)]

                    self.output.add(job_converter.convert(job))
        except KeyError as e:
            fail("Error building Job item. Key (%s) not found." % e)
        except Exception as e:
            fail("Error adding jobs to the output. Failed with exception: %s" % e)

    def __populate_output_by_queue(self, job_queue, job_status, expand_children, details):
        """
        Add Job items to the output asking for given queue and status.

        :param job_queue: job queue name or ARN
        :param job_status: list of job status to ask
        :param expand_children: if True, the job with children will be expanded by creating a row for each child
        :param details: ask for job details
        """
        try:
            single_jobs = []
            jobs_with_children = []
            for status in job_status:
                next_token = ""  # nosec
                while next_token is not None:
                    response = self.batch_client.list_jobs(jobStatus=status, jobQueue=job_queue, nextToken=next_token)

                    for job in response["jobSummaryList"]:
                        if get_job_type(job) != "SIMPLE" and expand_children is True:
                            jobs_with_children.append(job["jobId"])
                        else:
                            single_jobs.append(job)
                    next_token = response.get("nextToken")

            # create output items for job array children
            self.__populate_output_by_job_ids(jobs_with_children, details)

            # add single jobs to the output
            self.__add_jobs(single_jobs, details)

        except Exception as e:
            fail("Error listing jobs from AWS Batch. Failed with exception: %s" % e)
Ejemplo n.º 2
0
class AWSBstatCommand(object):
    """
    awsbstat command
    """
    def __init__(self, log, boto3_factory):
        """
        :param log: log
        :param boto3_factory: an initialized Boto3ClientFactory object
        """
        self.log = log
        mapping = collections.OrderedDict([('jobId', 'id'),
                                           ('jobName', 'name'),
                                           ('createdAt', 'creation_time'),
                                           ('startedAt', 'start_time'),
                                           ('stoppedAt', 'stop_time'),
                                           ('status', 'status'),
                                           ('statusReason', 'status_reason'),
                                           ('jobDefinition', 'job_definition'),
                                           ('jobQueue', 'queue'),
                                           ('command', 'command'),
                                           ('exitCode', 'exit_code'),
                                           ('reason', 'reason'),
                                           ('vcpus', 'vcpus'),
                                           ('memory[MB]', 'memory'),
                                           ('nodes', 'nodes'),
                                           ('logStream', 'log_stream'),
                                           ('log', 'log_stream_url')])
        self.output = Output(mapping=mapping)
        self.boto3_factory = boto3_factory
        self.batch_client = boto3_factory.get_client('batch')

    def run(self,
            job_status,
            expand_arrays,
            job_queue=None,
            job_ids=None,
            show_details=False):
        """
        print list of jobs, by filtering by queue or by ids
        """
        if job_ids:
            self.__populate_output_by_job_ids(
                job_status, job_ids, show_details or len(job_ids) == 1)
            # explicitly asking for job details,
            # or asking for a single job that is not an array (the output is not a list of jobs)
            details_required = show_details or (len(job_ids) == 1
                                                and self.output.length() == 1)
        elif job_queue:
            self.__populate_output_by_queue(job_queue, job_status,
                                            expand_arrays, show_details)
            details_required = show_details
        else:
            fail(
                "Error listing jobs from AWS Batch. job_ids or job_queue must be defined"
            )

        if details_required:
            self.output.show()
        else:
            self.output.show_table([
                'jobId', 'jobName', 'status', 'startedAt', 'stoppedAt',
                'exitCode'
            ])

    def __populate_output_by_job_ids(self, job_status, job_ids, details):
        """
        Add Job item or jobs array children to the output
        :param job_status: list of job status to ask
        :param job_ids: job ids or ARNs
        :param details: ask for job details
        """
        try:
            if job_ids:
                self.log.info("Describing jobs (%s), details (%s)" %
                              (job_ids, details))
                single_jobs = []
                job_array_ids = []
                jobs = self.batch_client.describe_jobs(jobs=job_ids)['jobs']
                for job in jobs:
                    if is_job_array(job):
                        job_array_ids.append(job['jobId'])
                    else:
                        single_jobs.append(job)

                # create output items for job array children
                self.__populate_output_by_array_ids(job_status, job_array_ids,
                                                    details)

                # add single jobs to the output
                self.__add_jobs(single_jobs, details)
        except Exception as e:
            fail(
                "Error describing jobs from AWS Batch. Failed with exception: %s"
                % e)

    def __populate_output_by_array_ids(self, job_status, job_array_ids,
                                       details):
        """
        Add jobs array children to the output
        :param job_status: list of job status to ask
        :param job_array_ids: job array ids to ask
        :param details: ask for job details
        """
        try:
            for job_array_id in job_array_ids:
                for status in job_status:
                    self.log.info(
                        "Listing job array children for job (%s) in status (%s)"
                        % (job_array_id, status))
                    next_token = ''
                    while next_token is not None:
                        response = self.batch_client.list_jobs(
                            jobStatus=status,
                            arrayJobId=job_array_id,
                            nextToken=next_token)
                        # add single jobs to the output
                        self.__add_jobs(response['jobSummaryList'], details)
                        next_token = response.get('nextToken')
        except Exception as e:
            fail(
                "Error listing job array children for job (%s). Failed with exception: %s"
                % (job_array_id, e))

    def __add_jobs(self, jobs, details):
        """
        Get job info from AWS Batch and add to the output
        :param jobs: list of jobs items (output of the list_jobs function)
        :param details: ask for job details
        """
        try:
            if jobs:
                self.log.debug("Adding jobs to the output (%s)" % jobs)
                if details:
                    self.log.info("Asking for jobs details")
                    jobs_to_show = []
                    for index in range(0, len(jobs), 100):
                        jobs_chunk = jobs[index:index + 100]
                        job_ids = []
                        for job in jobs_chunk:
                            job_ids.append(job['jobId'])
                        jobs_to_show.extend(
                            self.batch_client.describe_jobs(
                                jobs=job_ids)['jobs'])
                else:
                    jobs_to_show = jobs

                for job in jobs_to_show:
                    nodes = 1
                    if 'nodeProperties' in job:
                        # MNP job
                        container = job['nodeProperties'][
                            'nodeRangeProperties'][0]['container']
                        nodes = job['nodeProperties']['numNodes']
                    elif 'container' in job:
                        container = job['container']
                    else:
                        container = {}

                    if is_job_array(job):
                        # parent job array
                        job_id = '{0}[{1}]'.format(
                            job['jobId'], job['arrayProperties']['size'])
                        log_stream = '-'
                        log_stream_url = '-'
                    else:
                        job_id = job['jobId']
                        if 'logStreamName' in container:
                            log_stream = container.get('logStreamName')
                            log_stream_url = _compose_log_stream_url(
                                self.boto3_factory.region, log_stream)
                        else:
                            log_stream = '-'
                            log_stream_url = '-'

                    command = container.get('command', [])
                    self.log.debug("Adding job to the output (%s)", job)
                    job = Job(job_id=job_id,
                              name=job['jobName'],
                              creation_time=convert_to_date(job['createdAt']),
                              start_time=convert_to_date(job['startedAt'])
                              if 'startedAt' in job else '-',
                              stop_time=convert_to_date(job['stoppedAt'])
                              if 'stoppedAt' in job else '-',
                              status=job.get('status', 'UNKNOWN'),
                              status_reason=job.get('statusReason', '-'),
                              job_definition=get_job_definition_name_by_arn(
                                  job['jobDefinition'], version=True)
                              if 'jobQueue' in job else '-',
                              queue=job['jobQueue'].split('/')[1]
                              if 'jobQueue' in job else '-',
                              command=shell_join(command) if command else '-',
                              reason=container.get('reason', '-'),
                              exit_code=container.get('exitCode', '-'),
                              vcpus=container.get('vcpus', '-'),
                              memory=container.get('memory', '-'),
                              nodes=nodes,
                              log_stream=log_stream,
                              log_stream_url=log_stream_url)
                    self.output.add(job)
        except KeyError as e:
            fail("Error building Job item. Key (%s) not found." % e)
        except Exception as e:
            fail("Error adding jobs to the output. Failed with exception: %s" %
                 e)

    def __populate_output_by_queue(self, job_queue, job_status, expand_arrays,
                                   details):
        """
        Add Job items to the output asking for given queue and status
        :param job_queue: job queue name or ARN
        :param job_status: list of job status to ask
        :param expand_arrays: if True, the job array will be expanded by creating a row for each child
        :param details: ask for job details
        """
        try:
            for status in job_status:
                next_token = ''
                while next_token is not None:
                    response = self.batch_client.list_jobs(
                        jobStatus=status,
                        jobQueue=job_queue,
                        nextToken=next_token)
                    single_jobs = []
                    job_array_ids = []
                    for job in response['jobSummaryList']:
                        if is_job_array(job) and expand_arrays is True:
                            job_array_ids.append(job['jobId'])
                        else:
                            single_jobs.append(job)

                    # create output items for job array children
                    self.__populate_output_by_job_ids(job_status,
                                                      job_array_ids, details)

                    # add single jobs to the output
                    self.__add_jobs(single_jobs, details)

                    next_token = response.get('nextToken')
        except Exception as e:
            fail("Error listing jobs from AWS Batch. Failed with exception: %s"
                 % e)