PK!ecs_tool/__init__.pyPK!UBBecs_tool/cli.pyimport boto3 import click from botocore.exceptions import NoRegionError, NoCredentialsError from click import UsageError, ClickException from ecs_tool.ecs import ( fetch_services, fetch_tasks, fetch_task_definitions, run_ecs_task, task_logs, ) from ecs_tool.exceptions import ( NoResultsException, TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, NotSupportedLogDriver, NoLogStreamsFound, ) class EcsClusterCommand(click.core.Command): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.params.insert( 0, click.core.Option( ("--cluster",), default="default", help="Cluster name or ARN.", show_default=True, ), ) @click.group() @click.pass_context def cli(ctx): ctx.obj = {} try: ecs_client = boto3.client("ecs") logs_client = boto3.client("logs") except (NoRegionError, NoCredentialsError) as e: raise UsageError(f"AWS Configuration: {e}") ctx.obj["ecs_client"] = ecs_client ctx.obj["logs_client"] = logs_client @cli.command(cls=EcsClusterCommand) @click.option( "--launch-type", type=click.Choice(["EC2", "FARGATE"]), help="Launch type" ) @click.option( "--scheduling-strategy", type=click.Choice(["REPLICA", "DAEMON"]), help="Scheduling strategy", ) @click.pass_context def services(ctx, cluster, launch_type=None, scheduling_strategy=None): """ List of services. D - Desired count P - Pending count R - Running count """ try: result = fetch_services( ctx.obj["ecs_client"], cluster, launch_type, scheduling_strategy ) except NoResultsException as e: raise ClickException(e) print(result.table) @cli.command(cls=EcsClusterCommand) @click.option( "--status", type=click.Choice(["RUNNING", "STOPPED", "ANY"]), default="RUNNING", help="Task status", show_default=True, ) @click.option("--service-name", help="Service name") @click.option("--family", help="Family name") @click.option( "--launch-type", type=click.Choice(["EC2", "FARGATE"]), help="Launch type" ) @click.pass_context def tasks(ctx, cluster, status, service_name=None, family=None, launch_type=None): """ List of tasks. """ try: result = fetch_tasks( ctx.obj["ecs_client"], cluster, status, service_name, family, launch_type ) except NoResultsException as e: raise ClickException(e) print(result.table) @cli.command(cls=EcsClusterCommand) @click.argument("task", required=True) @click.pass_context def task_log(ctx, cluster, task): """ Display awslogs for task. task: Task id. """ try: result = task_logs(ctx.obj["ecs_client"], ctx.obj["logs_client"], cluster, task) except (NoResultsException, NotSupportedLogDriver, NoLogStreamsFound) as e: raise ClickException(e) print(result.table) @cli.command() @click.option("--family", help="Family name") @click.option("--status", type=click.Choice(["ACTIVE", "INACTIVE"]), help="Status") @click.pass_context def task_definitions(ctx, family=None, status=None): """ List of task definitions. """ try: result = fetch_task_definitions(ctx.obj["ecs_client"], family, status) except NoResultsException as e: raise ClickException(e) print(result.table) @cli.command(cls=EcsClusterCommand) @click.option("--wait", is_flag=True, help="Wait till task will reach STOPPED status.") @click.option("--wait-delay", default=3, help="Delay between task status check.") @click.option( "--wait-max-attempts", default=100, help="Maximum attempts to check if task finished.", ) @click.option("--logs", is_flag=True, help="Display logs from task after execution.") @click.argument("task-definition", required=True) @click.argument("command", nargs=-1) @click.pass_context def run_task( ctx, cluster, wait, wait_delay, wait_max_attempts, logs, task_definition, command=None, ): """ Run task. task_definition: Task definition.\n command: Command passed to task. Needs to be passed after "--" e.g. ecs run-task my_definition:1 -- my_script/ """ try: results = run_ecs_task( ctx.obj["ecs_client"], ctx.obj["logs_client"], cluster, task_definition, wait, wait_delay, wait_max_attempts, logs, command, ) for result in results: print(result.table) click.echo("\n") except ( TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, ) as e: raise ClickException(e) if __name__ == "__main__": cli() PK!3xecs_tool/ecs.pyimport itertools import botocore from ecs_tool.exceptions import ( NoResultsException, TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, NotSupportedLogDriver, NoLogStreamsFound, ) from ecs_tool.tables import ( TasksTable, TaskLogTable, ServicesTable, TaskDefinitionsTable, ) def _paginate(ecs_client, service, **kwargs): kwargs = {k: v for k, v in kwargs.items() if v is not None} paginator = ecs_client.get_paginator(service) pagination_config = {"MaxItems": 100, "PageSize": 100} resp = paginator.paginate(**kwargs, PaginationConfig=pagination_config) yield resp while "NextToken" in resp: yield paginator.paginate( { **kwargs, **{"PaginationConfig": pagination_config}, **{"StartingToken": resp["NextToken"]}, } ) def fetch_services(ecs_client, cluster, launch_type=None, scheduling_strategy=None): pagination = _paginate( ecs_client, "list_services", cluster=cluster, launchType=launch_type, schedulingStrategy=scheduling_strategy, ) arns = [] try: for iterator in pagination: for service in iterator: arns += service["serviceArns"] except ecs_client.exceptions.ClusterNotFoundException: raise NoResultsException("No results found.") if not arns: raise NoResultsException("No results found.") describe_services = ecs_client.describe_services(cluster=cluster, services=arns) return ServicesTable.build(describe_services["services"]) def fetch_tasks( ecs_client, cluster, status, service_name=None, family=None, launch_type=None ): if status == "ANY": pagination_running = _paginate( ecs_client, "list_tasks", cluster=cluster, desiredStatus="RUNNING", serviceName=service_name, family=family, launchType=launch_type, ) pagination_stopped = _paginate( ecs_client, "list_tasks", cluster=cluster, desiredStatus="STOPPED", serviceName=service_name, family=family, launchType=launch_type, ) pagination = itertools.chain(pagination_running, pagination_stopped) else: pagination = _paginate( ecs_client, "list_tasks", cluster=cluster, desiredStatus=status, serviceName=service_name, family=family, launchType=launch_type, ) arns = [] for iterator in pagination: for task in iterator: arns += task["taskArns"] if not arns: raise NoResultsException("No results found.") describe_services = ecs_client.describe_tasks(cluster=cluster, tasks=arns) return TasksTable.build(describe_services["tasks"]) def fetch_task_definitions(ecs_client, family, status): pagination = _paginate( ecs_client, "list_task_definitions", familyPrefix=family, status=status ) arns = [] for iterator in pagination: for task_definition in iterator: arns += task_definition["taskDefinitionArns"] if not arns: raise NoResultsException("No results found.") return TaskDefinitionsTable.build(arns) def task_logs(ecs_client, logs_client, cluster, task): tasks = ecs_client.describe_tasks(cluster=cluster, tasks=[task]) if not tasks["tasks"]: raise NoResultsException("No results found.") task_definition = ecs_client.describe_task_definition( taskDefinition=tasks["tasks"][0]["taskDefinitionArn"] ) log_configuration = task_definition["taskDefinition"]["containerDefinitions"][0][ "logConfiguration" ] if log_configuration["logDriver"] != "awslogs": raise NotSupportedLogDriver( f'Log driver "{log_configuration["logDriver"]}" is not supported yet.' ) describe_log_streams = logs_client.describe_log_streams( logGroupName=log_configuration["options"]["awslogs-group"], orderBy="LastEventTime", descending=True, limit=1, ) if not describe_log_streams["logStreams"]: raise NoLogStreamsFound("No logs found.") log_events = logs_client.get_log_events( logGroupName=log_configuration["options"]["awslogs-group"], logStreamName=describe_log_streams["logStreams"][0]["logStreamName"], limit=100, startFromHead=True, ) if not log_events["events"]: raise NoLogStreamsFound("No logs found.") return TaskLogTable.build(log_events["events"]) def run_ecs_task( ecs_client, logs_client, cluster, task_definition, wait, wait_delay, wait_max_attempts, logs, command=None, ): args = {"cluster": cluster} try: _, _ = task_definition.split(":") args["taskDefinition"] = task_definition except ValueError: args["taskDefinition"] = _fetch_latest_active_task_definition( ecs_client, task_definition ) if command: args["overrides"] = { "containerOverrides": [ {"name": task_definition.rsplit(":", 1)[0], "command": command} ] } try: result = ecs_client.run_task(**args) except ecs_client.exceptions.InvalidParameterException as e: raise TaskDefinitionInactiveException(e) describe_tasks = ecs_client.describe_tasks( cluster=cluster, tasks=(result["tasks"][0]["taskArn"],) ) yield TasksTable.build(describe_tasks["tasks"]) if wait: waiter = ecs_client.get_waiter("tasks_stopped") try: waiter.wait( cluster=cluster, tasks=(result["tasks"][0]["taskArn"],), WaiterConfig={"Delay": wait_delay, "MaxAttempts": wait_max_attempts}, ) except botocore.exceptions.WaiterError as e: raise WaiterException(e) describe_tasks = ecs_client.describe_tasks( cluster=cluster, tasks=(result["tasks"][0]["taskArn"],) ) yield TasksTable.build(describe_tasks["tasks"]) if logs: yield task_logs(ecs_client, logs_client, cluster, result["tasks"][0]["taskArn"]) def _fetch_latest_active_task_definition(ecs_client, name): response = ecs_client.list_task_definitions( familyPrefix=name, status="ACTIVE", sort="DESC", maxResults=1 ) if not response["taskDefinitionArns"]: raise NoTaskDefinitionFoundException( f'Unable to find active task definition for "{name}".' ) return response["taskDefinitionArns"][0].rsplit(":", 1)[0] PK! ecs_tool/exceptions.pyclass EcsToolException(Exception): pass class WaiterException(EcsToolException): """ Exception used when we reached maximum number of attempts and task didn't reach STOPPED status. """ class TaskDefinitionInactiveException(EcsToolException): """ Task definition is inactive, we can't run task. """ class NoTaskDefinitionFoundException(EcsToolException): """ No task definition found. """ class NoResultsException(EcsToolException): """ Exception raised when no results found """ class NotSupportedLogDriver(EcsToolException): """ Specified log driver is not supported yet. """ class NoLogStreamsFound(EcsToolException): """ No log streams are available for log group. """ PK!lecs_tool/tables.pyfrom textwrap import wrap from colorclass import Color from terminaltables import SingleTable SERVICE_STATUS_COLOUR = { "ACTIVE": "autogreen", "DRAINING": "autoyellow", "INACTIVE": "autored", } TASK_STATUS_COLOUR = { "PROVISIONING": "autoblue", "PENDING": "automagenta", "ACTIVATING": "autoyellow", "RUNNING": "autogreen", "DEACTIVATING": "autoyellow", "STOPPING": "automagenta", "DEPROVISIONING": "autoblue", "STOPPED": "autored", } DATE_FORMAT = "%Y-%m-%d %H:%M:%S %Z" class EcsTable(SingleTable): def __init__(self, data): super().__init__(data) self.inner_row_border = False self.inner_column_border = False self.outer_border = False self.inner_heading_row_border = True class ServicesTable(EcsTable): HEADER = ( "Service name", "Task definition", "Status", "D", "P", "R", "Service type", "Launch type", ) @classmethod def build(cls, services): data = [ServicesTable.HEADER] for service in services: status_colour = SERVICE_STATUS_COLOUR.get(service["status"]) data.append( [ service["serviceName"], service["taskDefinition"].rsplit("task-definition/", 1)[-1], Color( f"{{{status_colour}}}{service['status']}{{/{status_colour}}}" ), service["desiredCount"], service["pendingCount"], service["runningCount"], service["schedulingStrategy"], service["launchType"], ] ) return cls(data) class TasksTable(EcsTable): HEADER = ( "Task", "Task definition", "Status", "Command", "Started at", "Stopped at", "Execution time", "Termination reason", ) @classmethod def build(cls, tasks): data = [TasksTable.HEADER] for task in tasks: status_colour = TASK_STATUS_COLOUR.get(task["lastStatus"]) termination_code = ( "(" + str(task.get("containers")[0].get("exitCode")) + ")" if "exitCode" in task.get("containers")[0] else "" ) termination_reason = ( _wrap(task.get("stoppedReason"), 10) + " " + _wrap(task.get("containers")[0].get("reason"), 10) + termination_code ) data.append( [ task["taskArn"].rsplit("task/", 1)[-1], task["taskDefinitionArn"].rsplit("task-definition/", 1)[-1], Color( f"{{{status_colour}}}{task['lastStatus']}{{/{status_colour}}}" ), " ".join( task["overrides"]["containerOverrides"][0].get("command", "") ), task.get("startedAt").strftime(DATE_FORMAT) if task.get("startedAt") else "", task.get("stoppedAt").strftime(DATE_FORMAT) if task.get("stoppedAt") else "", task.get("stoppedAt") - task.get("startedAt") if all((task.get("startedAt"), task.get("stoppedAt"))) else "", termination_reason, ] ) return cls(data) class TaskDefinitionsTable(EcsTable): HEADER = ("Task definition",) @classmethod def build(cls, task_definitions): data = [TaskDefinitionsTable.HEADER] for definition in task_definitions: data.append([definition.rsplit("task-definition/", 1)[-1]]) return cls(data) class TaskLogTable(EcsTable): HEADER = ("Event",) @classmethod def build(cls, events): data = [TaskLogTable.HEADER] for event in events: data.append([event["message"]]) return cls(data) def _wrap(text, size): if not text: return "" return "\n".join(wrap(text, size)) PK!H. '()ecs_tool-0.9.0.dist-info/entry_points.txtN+I/N.,()JM.L+ PK!!O.. ecs_tool-0.9.0.dist-info/LICENSEMIT License Copyright (c) 2019 Daniel Ancuta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!Hu)GTUecs_tool-0.9.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H! !ecs_tool-0.9.0.dist-info/METADATAV]o6}ׯm] l -]K\(R%);{Hى]I<{s(l4zD~Q<"]Q^:Km] ۍlBK+-M4d\,]}NFnY0pEds9mD ƍReJ:99ke;wKc 7"J&WƎThɊ:oXMRh2<䂿ҲK; g4}sr.i녴F׬= F;x 2ўu"`eи® O OExF子OH"%M;cZpx߂[SZQ!LnG@c8e/ O#oś 7*(cPId`-jʋ=xv>lD'.` AY>~3fdޟL2\=zJU ߺ_ \fƖ%ܢ|;Bq--ñ|r!2sLXLa5r"HVc߮;p]}?i-)嶳o'ɓL5 uXb 0nhi8,ItTZFF ?7PgZʅZ v0VZrT T+*HbPV0f Bcs3D?9 Ȏ{s:j +9IH:wUmc] NodCª-b~J gevvhiX+G(~!Yi8Sf6u=T~h(_ ^H<;-x. ~*͔1*S>R!M4QAN]4)XL[*T;w m] \TĂhf4Կ YѹTTf"aYAef[-F5BhlBNEQj} rޑnG[!neLHK2(e7v! *8IPK!HYecs_tool-0.9.0.dist-info/RECORD}KjP,% RKdKZ"Lz޳w{YGi((Z31r mvNLFIVՃVp##%5\ 1l6Pcy+^\2<#zwPUdC0f 'n;Ѝ`-2ڵ2G^Ep<,)Z +2P)j˂W ,YٟQ,ǵҼ RZ}l !ՁR6NJzS2_ eHG[&PK!ecs_tool/__init__.pyPK!UBB2ecs_tool/cli.pyPK!3xecs_tool/ecs.pyPK! b.ecs_tool/exceptions.pyPK!l1ecs_tool/tables.pyPK!H. '()lBecs_tool-0.9.0.dist-info/entry_points.txtPK!!O.. Becs_tool-0.9.0.dist-info/LICENSEPK!Hu)GTUFGecs_tool-0.9.0.dist-info/WHEELPK!H! !Gecs_tool-0.9.0.dist-info/METADATAPK!HY6Lecs_tool-0.9.0.dist-info/RECORDPK NN