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)
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)