2015-09-09 23:09:46 +00:00
#!/usr/bin/python
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
2016-12-06 10:35:25 +00:00
ANSIBLE_METADATA = { ' status ' : [ ' preview ' ] ,
' supported_by ' : ' community ' ,
' version ' : ' 1.0 ' }
2015-09-09 23:09:46 +00:00
DOCUMENTATION = '''
- - -
module : ecs_taskdefinition
short_description : register a task definition in ecs
description :
2016-12-30 17:30:04 +00:00
- Registers or deregisters task definitions in the Amazon Web Services ( AWS ) EC2 Container Service ( ECS )
2016-12-08 05:34:16 +00:00
version_added : " 2.0 "
author : Mark Chance ( @Java1Guy )
2015-09-09 23:09:46 +00:00
requirements : [ json , boto , botocore , boto3 ]
options :
state :
description :
- State whether the task definition should exist or be deleted
required : true
2015-09-24 08:58:10 +00:00
choices : [ ' present ' , ' absent ' ]
2015-09-09 23:09:46 +00:00
arn :
description :
- The arn of the task description to delete
required : false
family :
2015-09-24 08:58:10 +00:00
description :
- A Name that would be given to the task definition
required : false
2015-09-09 23:09:46 +00:00
revision :
2015-09-24 08:58:10 +00:00
description :
- A revision number for the task definition
2015-09-09 23:09:46 +00:00
required : False
type : int
containers :
2015-09-24 08:58:10 +00:00
description :
2016-12-09 19:39:02 +00:00
- A list of containers definitions
2015-09-09 23:09:46 +00:00
required : False
type : list of dicts with container definitions
2016-12-19 18:16:35 +00:00
network_mode :
description :
- The Docker networking mode to use for the containers in the task .
required : false
default : bridge
choices : [ ' bridge ' , ' host ' , ' none ' ]
version_added : 2.3
task_role_arn :
description :
- The Amazon Resource Name ( ARN ) of the IAM role that containers in this task can assume . All containers in this task are granted the permissions that are specified in this role .
required : false
version_added : 2.3
2015-09-09 23:09:46 +00:00
volumes :
2015-09-24 08:58:10 +00:00
description :
- A list of names of volumes to be attached
2015-09-09 23:09:46 +00:00
required : False
type : list of name
2016-01-24 00:11:49 +00:00
extends_documentation_fragment :
- aws
- ec2
2015-09-09 23:09:46 +00:00
'''
EXAMPLES = '''
2016-12-30 17:30:04 +00:00
- name : Create task definition
2015-09-09 23:09:46 +00:00
ecs_taskdefinition :
containers :
- name : simple - app
cpu : 10
essential : true
image : " httpd:2.4 "
memory : 300
mountPoints :
- containerPath : / usr / local / apache2 / htdocs
sourceVolume : my - vol
portMappings :
- containerPort : 80
hostPort : 80
- name : busybox
command :
- " /bin/sh -c \" while true; do echo ' <html> <head> <title>Amazon ECS Sample App</title> <style>body { margin-top: 40px; background-color: #333;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> ' > top; /bin/date > date ; echo ' </div></body></html> ' > bottom; cat top date bottom > /usr/local/apache2/htdocs/index.html ; sleep 1; done \" "
cpu : 10
entryPoint :
- sh
- " -c "
essential : false
image : busybox
memory : 200
volumesFrom :
- sourceContainer : simple - app
volumes :
- name : my - vol
family : test - cluster - taskdef
state : present
register : task_output
'''
RETURN = '''
taskdefinition :
description : a reflection of the input parameters
type : dict inputs plus revision , status , taskDefinitionArn
'''
try :
import boto
import botocore
HAS_BOTO = True
except ImportError :
HAS_BOTO = False
try :
import boto3
HAS_BOTO3 = True
except ImportError :
HAS_BOTO3 = False
2016-10-23 20:41:03 +00:00
from ansible . module_utils . basic import AnsibleModule
2017-01-04 13:50:13 +00:00
from ansible . module_utils . ec2 import boto3_conn , camel_dict_to_snake_dict , ec2_argument_spec , get_aws_connection_info
2016-10-23 20:41:03 +00:00
2015-09-09 23:09:46 +00:00
class EcsTaskManager :
""" Handles ECS Tasks """
def __init__ ( self , module ) :
self . module = module
try :
region , ec2_url , aws_connect_kwargs = get_aws_connection_info ( module , boto3 = True )
if not region :
module . fail_json ( msg = " Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file " )
self . ecs = boto3_conn ( module , conn_type = ' client ' , resource = ' ecs ' , region = region , endpoint = ec2_url , * * aws_connect_kwargs )
2016-10-23 20:41:03 +00:00
except boto . exception . NoAuthHandlerFound as e :
module . fail_json ( msg = " Can ' t authorize connection - " % str ( e ) )
2015-09-09 23:09:46 +00:00
def describe_task ( self , task_name ) :
try :
response = self . ecs . describe_task_definition ( taskDefinition = task_name )
return response [ ' taskDefinition ' ]
except botocore . exceptions . ClientError :
return None
2016-12-19 18:16:35 +00:00
def register_task ( self , family , task_role_arn , network_mode , container_definitions , volumes ) :
2016-12-09 19:39:02 +00:00
validated_containers = [ ]
# Ensures the number parameters are int as required by boto
for container in container_definitions :
for param in ( ' memory ' , ' cpu ' , ' memoryReservation ' ) :
if param in container :
container [ param ] = int ( container [ param ] )
if ' portMappings ' in container :
for port_mapping in container [ ' portMappings ' ] :
for port in ( ' hostPort ' , ' containerPort ' ) :
if port in port_mapping :
port_mapping [ port ] = int ( port_mapping [ port ] )
validated_containers . append ( container )
2016-12-19 18:16:35 +00:00
try :
response = self . ecs . register_task_definition ( family = family ,
taskRoleArn = task_role_arn ,
networkMode = network_mode ,
containerDefinitions = container_definitions ,
volumes = volumes )
except botocore . exceptions . ClientError as e :
self . module . fail_json ( msg = e . message , * * camel_dict_to_snake_dict ( e . response ) )
2015-09-09 23:09:46 +00:00
return response [ ' taskDefinition ' ]
2016-06-01 20:41:15 +00:00
def describe_task_definitions ( self , family ) :
data = {
" taskDefinitionArns " : [ ] ,
" nextToken " : None
}
def fetch ( ) :
# Boto3 is weird about params passed, so only pass nextToken if we have a value
params = {
' familyPrefix ' : family
}
if data [ ' nextToken ' ] :
params [ ' nextToken ' ] = data [ ' nextToken ' ]
result = self . ecs . list_task_definitions ( * * params )
data [ ' taskDefinitionArns ' ] + = result [ ' taskDefinitionArns ' ]
data [ ' nextToken ' ] = result . get ( ' nextToken ' , None )
return data [ ' nextToken ' ] is not None
# Fetch all the arns, possibly across multiple pages
while fetch ( ) :
pass
# Return the full descriptions of the task definitions, sorted ascending by revision
return list ( sorted ( [ self . ecs . describe_task_definition ( taskDefinition = arn ) [ ' taskDefinition ' ] for arn in data [ ' taskDefinitionArns ' ] ] , key = lambda td : td [ ' revision ' ] ) )
2015-09-09 23:09:46 +00:00
def deregister_task ( self , taskArn ) :
response = self . ecs . deregister_task_definition ( taskDefinition = taskArn )
return response [ ' taskDefinition ' ]
2016-12-19 18:16:35 +00:00
2015-09-09 23:09:46 +00:00
def main ( ) :
argument_spec = ec2_argument_spec ( )
argument_spec . update ( dict (
2016-06-01 20:41:15 +00:00
state = dict ( required = True , choices = [ ' present ' , ' absent ' ] ) ,
arn = dict ( required = False , type = ' str ' ) ,
family = dict ( required = False , type = ' str ' ) ,
revision = dict ( required = False , type = ' int ' ) ,
containers = dict ( required = False , type = ' list ' ) ,
2016-12-19 18:16:35 +00:00
network_mode = dict ( required = False , default = ' bridge ' , choices = [ ' bridge ' , ' host ' , ' none ' ] , type = ' str ' ) ,
task_role_arn = dict ( required = False , default = ' ' , type = ' str ' ) ,
volumes = dict ( required = False , type = ' list ' ) ) )
2015-09-09 23:09:46 +00:00
module = AnsibleModule ( argument_spec = argument_spec , supports_check_mode = True )
if not HAS_BOTO :
2017-01-30 23:01:47 +00:00
module . fail_json ( msg = ' boto is required. ' )
2015-09-09 23:09:46 +00:00
if not HAS_BOTO3 :
2017-01-30 23:01:47 +00:00
module . fail_json ( msg = ' boto3 is required. ' )
2015-09-09 23:09:46 +00:00
task_to_describe = None
2016-06-01 20:41:15 +00:00
task_mgr = EcsTaskManager ( module )
results = dict ( changed = False )
2015-09-09 23:09:46 +00:00
if module . params [ ' state ' ] == ' present ' :
2016-06-01 20:41:15 +00:00
if ' containers ' not in module . params or not module . params [ ' containers ' ] :
2015-09-09 23:09:46 +00:00
module . fail_json ( msg = " To use task definitions, a list of containers must be specified " )
2016-06-01 20:41:15 +00:00
if ' family ' not in module . params or not module . params [ ' family ' ] :
module . fail_json ( msg = " To use task definitions, a family must be specified " )
2015-09-09 23:09:46 +00:00
2016-06-01 20:41:15 +00:00
family = module . params [ ' family ' ]
existing_definitions_in_family = task_mgr . describe_task_definitions ( module . params [ ' family ' ] )
if ' revision ' in module . params and module . params [ ' revision ' ] :
# The definition specifies revision. We must gurantee that an active revision of that number will result from this.
revision = int ( module . params [ ' revision ' ] )
# A revision has been explicitly specified. Attempt to locate a matching revision
tasks_defs_for_revision = [ td for td in existing_definitions_in_family if td [ ' revision ' ] == revision ]
existing = tasks_defs_for_revision [ 0 ] if len ( tasks_defs_for_revision ) > 0 else None
if existing and existing [ ' status ' ] != " ACTIVE " :
# We cannot reactivate an inactive revision
module . fail_json ( msg = " A task in family ' %s ' already exists for revsion %d , but it is inactive " % ( family , revision ) )
elif not existing :
2017-01-24 16:19:22 +00:00
if not existing_definitions_in_family and revision != 1 :
2016-06-01 20:41:15 +00:00
module . fail_json ( msg = " You have specified a revision of %d but a created revision would be 1 " % revision )
2017-01-24 16:19:22 +00:00
elif existing_definitions_in_family and existing_definitions_in_family [ - 1 ] [ ' revision ' ] + 1 != revision :
2016-06-01 20:41:15 +00:00
module . fail_json ( msg = " You have specified a revision of %d but a created revision would be %d " % ( revision , existing_definitions_in_family [ - 1 ] [ ' revision ' ] + 1 ) )
else :
existing = None
def _right_has_values_of_left ( left , right ) :
# Make sure the values are equivalent for everything left has
2016-12-12 23:16:23 +00:00
for k , v in left . items ( ) :
2016-06-01 20:41:15 +00:00
if not ( ( not v and ( k not in right or not right [ k ] ) ) or ( k in right and v == right [ k ] ) ) :
# We don't care about list ordering because ECS can change things
if isinstance ( v , list ) and k in right :
left_list = v
right_list = right [ k ] or [ ]
if len ( left_list ) != len ( right_list ) :
return False
for list_val in left_list :
if list_val not in right_list :
return False
else :
return False
# Make sure right doesn't have anything that left doesn't
2016-12-12 23:16:23 +00:00
for k , v in right . items ( ) :
2016-06-01 20:41:15 +00:00
if v and k not in left :
return False
return True
def _task_definition_matches ( requested_volumes , requested_containers , existing_task_definition ) :
if td [ ' status ' ] != " ACTIVE " :
return None
existing_volumes = td . get ( ' volumes ' , [ ] ) or [ ]
if len ( requested_volumes ) != len ( existing_volumes ) :
# Nope.
return None
if len ( requested_volumes ) > 0 :
for requested_vol in requested_volumes :
found = False
for actual_vol in existing_volumes :
if _right_has_values_of_left ( requested_vol , actual_vol ) :
found = True
break
if not found :
return None
existing_containers = td . get ( ' containerDefinitions ' , [ ] ) or [ ]
if len ( requested_containers ) != len ( existing_containers ) :
# Nope.
return None
for requested_container in requested_containers :
found = False
for actual_container in existing_containers :
if _right_has_values_of_left ( requested_container , actual_container ) :
found = True
break
if not found :
return None
return existing_task_definition
# No revision explicitly specified. Attempt to find an active, matching revision that has all the properties requested
for td in existing_definitions_in_family :
requested_volumes = module . params . get ( ' volumes ' , [ ] ) or [ ]
requested_containers = module . params . get ( ' containers ' , [ ] ) or [ ]
existing = _task_definition_matches ( requested_volumes , requested_containers , td )
if existing :
break
if existing :
# Awesome. Have an existing one. Nothing to do.
results [ ' taskdefinition ' ] = existing
2015-09-09 23:09:46 +00:00
else :
if not module . check_mode :
2016-06-01 20:41:15 +00:00
# Doesn't exist. create it.
volumes = module . params . get ( ' volumes ' , [ ] ) or [ ]
2015-09-09 23:09:46 +00:00
results [ ' taskdefinition ' ] = task_mgr . register_task ( module . params [ ' family ' ] ,
2016-12-19 18:16:35 +00:00
module . params [ ' task_role_arn ' ] ,
module . params [ ' network_mode ' ] ,
module . params [ ' containers ' ] ,
volumes )
2015-09-09 23:09:46 +00:00
results [ ' changed ' ] = True
elif module . params [ ' state ' ] == ' absent ' :
2016-06-01 20:41:15 +00:00
# When de-registering a task definition, we can specify the ARN OR the family and revision.
if module . params [ ' state ' ] == ' absent ' :
if ' arn ' in module . params and module . params [ ' arn ' ] is not None :
task_to_describe = module . params [ ' arn ' ]
elif ' family ' in module . params and module . params [ ' family ' ] is not None and ' revision ' in module . params and \
module . params [ ' revision ' ] is not None :
task_to_describe = module . params [ ' family ' ] + " : " + str ( module . params [ ' revision ' ] )
else :
module . fail_json ( msg = " To use task definitions, an arn or family and revision must be specified " )
existing = task_mgr . describe_task ( task_to_describe )
2015-09-09 23:09:46 +00:00
if not existing :
pass
else :
2016-06-01 20:41:15 +00:00
# It exists, so we should delete it and mark changed. Return info about the task definition deleted
2015-09-09 23:09:46 +00:00
results [ ' taskdefinition ' ] = existing
2016-06-01 20:41:15 +00:00
if ' status ' in existing and existing [ ' status ' ] == " INACTIVE " :
2015-09-09 23:09:46 +00:00
results [ ' changed ' ] = False
else :
if not module . check_mode :
task_mgr . deregister_task ( task_to_describe )
results [ ' changed ' ] = True
module . exit_json ( * * results )
if __name__ == ' __main__ ' :
main ( )