PK!ecs_tool/__init__.pyPK!b!!ecs_tool/cli.pyimport sys import boto3 import click from botocore.exceptions import NoRegionError, NoCredentialsError from click import UsageError from ecs_tool.ecs import ( fetch_services, fetch_tasks, fetch_task_definitions, run_ecs_task, ) from ecs_tool.exceptions import ( NoResultsException, TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, ) from ecs_tool.tables import ServicesTable, TasksTable, TaskDefinitionsTable 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") except (NoRegionError, NoCredentialsError) as e: raise UsageError(f"AWS Configuration: {e}") ctx.obj["ecs_client"] = ecs_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: click.secho("No results found.", fg="red") sys.exit() print(ServicesTable.build(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: click.secho("No results found.", fg="red") sys.exit() print(TasksTable.build(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: click.secho("No results found.", fg="red") sys.exit() print(TaskDefinitionsTable.build(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.argument("task-definition", required=True) @click.argument("command", nargs=-1) @click.pass_context def run_task( ctx, cluster, wait, wait_delay, wait_max_attempts, 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: result = run_ecs_task( ctx.obj["ecs_client"], cluster, task_definition, wait, wait_delay, wait_max_attempts, command, ) except ( TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, ) as e: click.secho(str(e), fg="red") sys.exit(1) print(TasksTable.build(result).table) if __name__ == "__main__": cli() PK!K{{ecs_tool/ecs.pyimport itertools import botocore from ecs_tool.exceptions import ( NoResultsException, TaskDefinitionInactiveException, WaiterException, NoTaskDefinitionFoundException, ) 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 = [] for iterator in pagination: for service in iterator: arns += service["serviceArns"] if not arns: raise NoResultsException describe_services = ecs_client.describe_services(cluster=cluster, services=arns) return 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 describe_services = ecs_client.describe_tasks(cluster=cluster, tasks=arns) return 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 return arns def run_ecs_task( ecs_client, cluster, task_definition, wait, wait_delay, wait_max_attempts, 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) 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"],) ) return describe_tasks["tasks"] 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!)?Decs_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 """ PK!F@7FFecs_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 = 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) def _wrap(text, size): if not text: return "" return "\n".join(wrap(text, size)) PK!H. '()ecs_tool-0.6.0.dist-info/entry_points.txtN+I/N.,()JM.L+ PK!!O.. ecs_tool-0.6.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.6.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H묲[!ecs_tool-0.6.0.dist-info/METADATAUQo6~篸KJ!݄t l{0gȓ]=qRoE}(~H$?eMT&Z<읤g鉘M#} v^:]@$ 0yEh=/Ɋ3D.YV)UZ&*h>{Ps 4ogwWv֗!:b̨JL[JhIMJps1Z1$7#頋Iz6:M_jZ+oVyk49`5AfДX´-{x[Ԗ#콡x7u*-(8$e*ws9k.5ti"ZjGmT|/Ɍgx尲d'i| ~)62=9QI|gmR\i졓o`cA9ܢA: C# 7w%wØ=e|7!rwxx*]œ$LVPa[E#C7쑡* 5_2fs*%$|#}p-u B|Ҕ౉[܃` 1ϫ a<X*D|S*iIktBhKߟ1;9񺥘h)@-Yը3lOrrjc?-o PK!HUecs_tool-0.6.0.dist-info/RECORD}IP}@E-(ho^}_n$4!BT! y5Zs+(irQz } IΐI[Vc~7A}/).7qR`ts+P ]NdEȹߛ2eD<3nFmr+n=I %Pg~7ԁ08ₑ#0™ܩ2J%9jsжaz9}X҇=1[ѥHzdT{ /^3gx͖txe@ ZUp FCԊ_QDUVQ!I!6օwIfSnFv5n6:~M]^kfβcbk"mns.;F|M P|-$rNIXXy%шBȯnbzbP eo^QSeSUy@رE vvIOҧ\ ] PK!ecs_tool/__init__.pyPK!b!!2ecs_tool/cli.pyPK!K{{ecs_tool/ecs.pyPK!)?D($ecs_tool/exceptions.pyPK!F@7FFw&ecs_tool/tables.pyPK!H. '()5ecs_tool-0.6.0.dist-info/entry_points.txtPK!!O.. [6ecs_tool-0.6.0.dist-info/LICENSEPK!Hu)GTU:ecs_tool-0.6.0.dist-info/WHEELPK!H묲[!W;ecs_tool-0.6.0.dist-info/METADATAPK!HU>ecs_tool-0.6.0.dist-info/RECORDPK  A