1# Copyright (c) 2015 Amazon.com, Inc. or its affiliates.  All Rights Reserved
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21#
22
23import boto
24from boto.compat import json
25from boto.connection import AWSQueryConnection
26from boto.regioninfo import RegionInfo
27from boto.exception import JSONResponseError
28from boto.ec2containerservice import exceptions
29
30
31class EC2ContainerServiceConnection(AWSQueryConnection):
32    """
33    Amazon EC2 Container Service (Amazon ECS) is a highly scalable,
34    fast, container management service that makes it easy to run,
35    stop, and manage Docker containers on a cluster of Amazon EC2
36    instances. Amazon ECS lets you launch and stop container-enabled
37    applications with simple API calls, allows you to get the state of
38    your cluster from a centralized service, and gives you access to
39    many familiar Amazon EC2 features like security groups, Amazon EBS
40    volumes, and IAM roles.
41
42    You can use Amazon ECS to schedule the placement of containers
43    across your cluster based on your resource needs, isolation
44    policies, and availability requirements. Amazon EC2 Container
45    Service eliminates the need for you to operate your own cluster
46    management and configuration management systems or worry about
47    scaling your management infrastructure.
48    """
49    APIVersion = "2014-11-13"
50    DefaultRegionName = "us-east-1"
51    DefaultRegionEndpoint = "ecs.us-east-1.amazonaws.com"
52    ResponseError = JSONResponseError
53
54    _faults = {
55        "ServerException": exceptions.ServerException,
56        "ClientException": exceptions.ClientException,
57    }
58
59
60    def __init__(self, **kwargs):
61        region = kwargs.pop('region', None)
62        if not region:
63            region = RegionInfo(self, self.DefaultRegionName,
64                                self.DefaultRegionEndpoint)
65
66        if 'host' not in kwargs or kwargs['host'] is None:
67            kwargs['host'] = region.endpoint
68
69        super(EC2ContainerServiceConnection, self).__init__(**kwargs)
70        self.region = region
71
72    def _required_auth_capability(self):
73        return ['hmac-v4']
74
75    def create_cluster(self, cluster_name=None):
76        """
77        Creates a new Amazon ECS cluster. By default, your account
78        will receive a `default` cluster when you launch your first
79        container instance. However, you can create your own cluster
80        with a unique name with the `CreateCluster` action.
81
82        During the preview, each account is limited to two clusters.
83
84        :type cluster_name: string
85        :param cluster_name: The name of your cluster. If you do not specify a
86            name for your cluster, you will create a cluster named `default`.
87
88        """
89        params = {}
90        if cluster_name is not None:
91            params['clusterName'] = cluster_name
92        return self._make_request(
93            action='CreateCluster',
94            verb='POST',
95            path='/', params=params)
96
97    def delete_cluster(self, cluster):
98        """
99        Deletes the specified cluster. You must deregister all
100        container instances from this cluster before you may delete
101        it. You can list the container instances in a cluster with
102        ListContainerInstances and deregister them with
103        DeregisterContainerInstance.
104
105        :type cluster: string
106        :param cluster: The cluster you want to delete.
107
108        """
109        params = {'cluster': cluster, }
110        return self._make_request(
111            action='DeleteCluster',
112            verb='POST',
113            path='/', params=params)
114
115    def deregister_container_instance(self, container_instance, cluster=None,
116                                      force=None):
117        """
118        Deregisters an Amazon ECS container instance from the
119        specified cluster. This instance will no longer be available
120        to run tasks.
121
122        :type cluster: string
123        :param cluster: The short name or full Amazon Resource Name (ARN) of
124            the cluster that hosts the container instance you want to
125            deregister. If you do not specify a cluster, the default cluster is
126            assumed.
127
128        :type container_instance: string
129        :param container_instance: The container instance UUID or full Amazon
130            Resource Name (ARN) of the container instance you want to
131            deregister. The ARN contains the `arn:aws:ecs` namespace, followed
132            by the region of the container instance, the AWS account ID of the
133            container instance owner, the `container-instance` namespace, and
134            then the container instance UUID. For example, arn:aws:ecs: region
135            : aws_account_id :container-instance/ container_instance_UUID .
136
137        :type force: boolean
138        :param force: Force the deregistration of the container instance. You
139            can use the `force` parameter if you have several tasks running on
140            a container instance and you don't want to run `StopTask` for each
141            task before deregistering the container instance.
142
143        """
144        params = {'containerInstance': container_instance, }
145        if cluster is not None:
146            params['cluster'] = cluster
147        if force is not None:
148            params['force'] = str(
149                force).lower()
150        return self._make_request(
151            action='DeregisterContainerInstance',
152            verb='POST',
153            path='/', params=params)
154
155    def deregister_task_definition(self, task_definition):
156        """
157        Deregisters the specified task definition. You will no longer
158        be able to run tasks from this definition after
159        deregistration.
160
161        :type task_definition: string
162        :param task_definition: The `family` and `revision` (
163            `family:revision`) or full Amazon Resource Name (ARN) of the task
164            definition that you want to deregister.
165
166        """
167        params = {'taskDefinition': task_definition, }
168        return self._make_request(
169            action='DeregisterTaskDefinition',
170            verb='POST',
171            path='/', params=params)
172
173    def describe_clusters(self, clusters=None):
174        """
175        Describes one or more of your clusters.
176
177        :type clusters: list
178        :param clusters: A space-separated list of cluster names or full
179            cluster Amazon Resource Name (ARN) entries. If you do not specify a
180            cluster, the default cluster is assumed.
181
182        """
183        params = {}
184        if clusters is not None:
185            self.build_list_params(params,
186                                   clusters,
187                                   'clusters.member')
188        return self._make_request(
189            action='DescribeClusters',
190            verb='POST',
191            path='/', params=params)
192
193    def describe_container_instances(self, container_instances, cluster=None):
194        """
195        Describes Amazon EC2 Container Service container instances.
196        Returns metadata about registered and remaining resources on
197        each container instance requested.
198
199        :type cluster: string
200        :param cluster: The short name or full Amazon Resource Name (ARN) of
201            the cluster that hosts the container instances you want to
202            describe. If you do not specify a cluster, the default cluster is
203            assumed.
204
205        :type container_instances: list
206        :param container_instances: A space-separated list of container
207            instance UUIDs or full Amazon Resource Name (ARN) entries.
208
209        """
210        params = {}
211        self.build_list_params(params,
212                               container_instances,
213                               'containerInstances.member')
214        if cluster is not None:
215            params['cluster'] = cluster
216        return self._make_request(
217            action='DescribeContainerInstances',
218            verb='POST',
219            path='/', params=params)
220
221    def describe_task_definition(self, task_definition):
222        """
223        Describes a task definition.
224
225        :type task_definition: string
226        :param task_definition: The `family` and `revision` (
227            `family:revision`) or full Amazon Resource Name (ARN) of the task
228            definition that you want to describe.
229
230        """
231        params = {'taskDefinition': task_definition, }
232        return self._make_request(
233            action='DescribeTaskDefinition',
234            verb='POST',
235            path='/', params=params)
236
237    def describe_tasks(self, tasks, cluster=None):
238        """
239        Describes a specified task or tasks.
240
241        :type cluster: string
242        :param cluster: The short name or full Amazon Resource Name (ARN) of
243            the cluster that hosts the task you want to describe. If you do not
244            specify a cluster, the default cluster is assumed.
245
246        :type tasks: list
247        :param tasks: A space-separated list of task UUIDs or full Amazon
248            Resource Name (ARN) entries.
249
250        """
251        params = {}
252        self.build_list_params(params,
253                               tasks,
254                               'tasks.member')
255        if cluster is not None:
256            params['cluster'] = cluster
257        return self._make_request(
258            action='DescribeTasks',
259            verb='POST',
260            path='/', params=params)
261
262    def discover_poll_endpoint(self, container_instance=None):
263        """
264        This action is only used by the Amazon EC2 Container Service
265        agent, and it is not intended for use outside of the agent.
266
267
268        Returns an endpoint for the Amazon EC2 Container Service agent
269        to poll for updates.
270
271        :type container_instance: string
272        :param container_instance: The container instance UUID or full Amazon
273            Resource Name (ARN) of the container instance. The ARN contains the
274            `arn:aws:ecs` namespace, followed by the region of the container
275            instance, the AWS account ID of the container instance owner, the
276            `container-instance` namespace, and then the container instance
277            UUID. For example, arn:aws:ecs: region : aws_account_id :container-
278            instance/ container_instance_UUID .
279
280        """
281        params = {}
282        if container_instance is not None:
283            params['containerInstance'] = container_instance
284        return self._make_request(
285            action='DiscoverPollEndpoint',
286            verb='POST',
287            path='/', params=params)
288
289    def list_clusters(self, next_token=None, max_results=None):
290        """
291        Returns a list of existing clusters.
292
293        :type next_token: string
294        :param next_token: The `nextToken` value returned from a previous
295            paginated `ListClusters` request where `maxResults` was used and
296            the results exceeded the value of that parameter. Pagination
297            continues from the end of the previous results that returned the
298            `nextToken` value. This value is `null` when there are no more
299            results to return.
300
301        :type max_results: integer
302        :param max_results: The maximum number of cluster results returned by
303            `ListClusters` in paginated output. When this parameter is used,
304            `ListClusters` only returns `maxResults` results in a single page
305            along with a `nextToken` response element. The remaining results of
306            the initial request can be seen by sending another `ListClusters`
307            request with the returned `nextToken` value. This value can be
308            between 1 and 100. If this parameter is not used, then
309            `ListClusters` returns up to 100 results and a `nextToken` value if
310            applicable.
311
312        """
313        params = {}
314        if next_token is not None:
315            params['nextToken'] = next_token
316        if max_results is not None:
317            params['maxResults'] = max_results
318        return self._make_request(
319            action='ListClusters',
320            verb='POST',
321            path='/', params=params)
322
323    def list_container_instances(self, cluster=None, next_token=None,
324                                 max_results=None):
325        """
326        Returns a list of container instances in a specified cluster.
327
328        :type cluster: string
329        :param cluster: The short name or full Amazon Resource Name (ARN) of
330            the cluster that hosts the container instances you want to list. If
331            you do not specify a cluster, the default cluster is assumed..
332
333        :type next_token: string
334        :param next_token: The `nextToken` value returned from a previous
335            paginated `ListContainerInstances` request where `maxResults` was
336            used and the results exceeded the value of that parameter.
337            Pagination continues from the end of the previous results that
338            returned the `nextToken` value. This value is `null` when there are
339            no more results to return.
340
341        :type max_results: integer
342        :param max_results: The maximum number of container instance results
343            returned by `ListContainerInstances` in paginated output. When this
344            parameter is used, `ListContainerInstances` only returns
345            `maxResults` results in a single page along with a `nextToken`
346            response element. The remaining results of the initial request can
347            be seen by sending another `ListContainerInstances` request with
348            the returned `nextToken` value. This value can be between 1 and
349            100. If this parameter is not used, then `ListContainerInstances`
350            returns up to 100 results and a `nextToken` value if applicable.
351
352        """
353        params = {}
354        if cluster is not None:
355            params['cluster'] = cluster
356        if next_token is not None:
357            params['nextToken'] = next_token
358        if max_results is not None:
359            params['maxResults'] = max_results
360        return self._make_request(
361            action='ListContainerInstances',
362            verb='POST',
363            path='/', params=params)
364
365    def list_task_definitions(self, family_prefix=None, next_token=None,
366                              max_results=None):
367        """
368        Returns a list of task definitions that are registered to your
369        account. You can filter the results by family name with the
370        `familyPrefix` parameter.
371
372        :type family_prefix: string
373        :param family_prefix: The name of the family that you want to filter
374            the `ListTaskDefinitions` results with. Specifying a `familyPrefix`
375            will limit the listed task definitions to definitions that belong
376            to that family.
377
378        :type next_token: string
379        :param next_token: The `nextToken` value returned from a previous
380            paginated `ListTaskDefinitions` request where `maxResults` was used
381            and the results exceeded the value of that parameter. Pagination
382            continues from the end of the previous results that returned the
383            `nextToken` value. This value is `null` when there are no more
384            results to return.
385
386        :type max_results: integer
387        :param max_results: The maximum number of task definition results
388            returned by `ListTaskDefinitions` in paginated output. When this
389            parameter is used, `ListTaskDefinitions` only returns `maxResults`
390            results in a single page along with a `nextToken` response element.
391            The remaining results of the initial request can be seen by sending
392            another `ListTaskDefinitions` request with the returned `nextToken`
393            value. This value can be between 1 and 100. If this parameter is
394            not used, then `ListTaskDefinitions` returns up to 100 results and
395            a `nextToken` value if applicable.
396
397        """
398        params = {}
399        if family_prefix is not None:
400            params['familyPrefix'] = family_prefix
401        if next_token is not None:
402            params['nextToken'] = next_token
403        if max_results is not None:
404            params['maxResults'] = max_results
405        return self._make_request(
406            action='ListTaskDefinitions',
407            verb='POST',
408            path='/', params=params)
409
410    def list_tasks(self, cluster=None, container_instance=None, family=None,
411                   next_token=None, max_results=None):
412        """
413        Returns a list of tasks for a specified cluster. You can
414        filter the results by family name or by a particular container
415        instance with the `family` and `containerInstance` parameters.
416
417        :type cluster: string
418        :param cluster: The short name or full Amazon Resource Name (ARN) of
419            the cluster that hosts the tasks you want to list. If you do not
420            specify a cluster, the default cluster is assumed..
421
422        :type container_instance: string
423        :param container_instance: The container instance UUID or full Amazon
424            Resource Name (ARN) of the container instance that you want to
425            filter the `ListTasks` results with. Specifying a
426            `containerInstance` will limit the results to tasks that belong to
427            that container instance.
428
429        :type family: string
430        :param family: The name of the family that you want to filter the
431            `ListTasks` results with. Specifying a `family` will limit the
432            results to tasks that belong to that family.
433
434        :type next_token: string
435        :param next_token: The `nextToken` value returned from a previous
436            paginated `ListTasks` request where `maxResults` was used and the
437            results exceeded the value of that parameter. Pagination continues
438            from the end of the previous results that returned the `nextToken`
439            value. This value is `null` when there are no more results to
440            return.
441
442        :type max_results: integer
443        :param max_results: The maximum number of task results returned by
444            `ListTasks` in paginated output. When this parameter is used,
445            `ListTasks` only returns `maxResults` results in a single page
446            along with a `nextToken` response element. The remaining results of
447            the initial request can be seen by sending another `ListTasks`
448            request with the returned `nextToken` value. This value can be
449            between 1 and 100. If this parameter is not used, then `ListTasks`
450            returns up to 100 results and a `nextToken` value if applicable.
451
452        """
453        params = {}
454        if cluster is not None:
455            params['cluster'] = cluster
456        if container_instance is not None:
457            params['containerInstance'] = container_instance
458        if family is not None:
459            params['family'] = family
460        if next_token is not None:
461            params['nextToken'] = next_token
462        if max_results is not None:
463            params['maxResults'] = max_results
464        return self._make_request(
465            action='ListTasks',
466            verb='POST',
467            path='/', params=params)
468
469    def register_container_instance(self, cluster=None,
470                                    instance_identity_document=None,
471                                    instance_identity_document_signature=None,
472                                    total_resources=None):
473        """
474        This action is only used by the Amazon EC2 Container Service
475        agent, and it is not intended for use outside of the agent.
476
477
478        Registers an Amazon EC2 instance into the specified cluster.
479        This instance will become available to place containers on.
480
481        :type cluster: string
482        :param cluster: The short name or full Amazon Resource Name (ARN) of
483            the cluster that you want to register your container instance with.
484            If you do not specify a cluster, the default cluster is assumed..
485
486        :type instance_identity_document: string
487        :param instance_identity_document:
488
489        :type instance_identity_document_signature: string
490        :param instance_identity_document_signature:
491
492        :type total_resources: list
493        :param total_resources:
494
495        """
496        params = {}
497        if cluster is not None:
498            params['cluster'] = cluster
499        if instance_identity_document is not None:
500            params['instanceIdentityDocument'] = instance_identity_document
501        if instance_identity_document_signature is not None:
502            params['instanceIdentityDocumentSignature'] = instance_identity_document_signature
503        if total_resources is not None:
504            self.build_complex_list_params(
505                params, total_resources,
506                'totalResources.member',
507                ('name', 'type', 'doubleValue', 'longValue', 'integerValue', 'stringSetValue'))
508        return self._make_request(
509            action='RegisterContainerInstance',
510            verb='POST',
511            path='/', params=params)
512
513    def register_task_definition(self, family, container_definitions):
514        """
515        Registers a new task definition from the supplied `family` and
516        `containerDefinitions`.
517
518        :type family: string
519        :param family: You can specify a `family` for a task definition, which
520            allows you to track multiple versions of the same task definition.
521            You can think of the `family` as a name for your task definition.
522
523        :type container_definitions: list
524        :param container_definitions: A list of container definitions in JSON
525            format that describe the different containers that make up your
526            task.
527
528        """
529        params = {'family': family, }
530        self.build_complex_list_params(
531            params, container_definitions,
532            'containerDefinitions.member',
533            ('name', 'image', 'cpu', 'memory', 'links', 'portMappings', 'essential', 'entryPoint', 'command', 'environment'))
534        return self._make_request(
535            action='RegisterTaskDefinition',
536            verb='POST',
537            path='/', params=params)
538
539    def run_task(self, task_definition, cluster=None, overrides=None,
540                 count=None):
541        """
542        Start a task using random placement and the default Amazon ECS
543        scheduler. If you want to use your own scheduler or place a
544        task on a specific container instance, use `StartTask`
545        instead.
546
547        :type cluster: string
548        :param cluster: The short name or full Amazon Resource Name (ARN) of
549            the cluster that you want to run your task on. If you do not
550            specify a cluster, the default cluster is assumed..
551
552        :type task_definition: string
553        :param task_definition: The `family` and `revision` (
554            `family:revision`) or full Amazon Resource Name (ARN) of the task
555            definition that you want to run.
556
557        :type overrides: dict
558        :param overrides:
559
560        :type count: integer
561        :param count: The number of instances of the specified task that you
562            would like to place on your cluster.
563
564        """
565        params = {'taskDefinition': task_definition, }
566        if cluster is not None:
567            params['cluster'] = cluster
568        if overrides is not None:
569            params['overrides'] = overrides
570        if count is not None:
571            params['count'] = count
572        return self._make_request(
573            action='RunTask',
574            verb='POST',
575            path='/', params=params)
576
577    def start_task(self, task_definition, container_instances, cluster=None,
578                   overrides=None):
579        """
580        Starts a new task from the specified task definition on the
581        specified container instance or instances. If you want to use
582        the default Amazon ECS scheduler to place your task, use
583        `RunTask` instead.
584
585        :type cluster: string
586        :param cluster: The short name or full Amazon Resource Name (ARN) of
587            the cluster that you want to start your task on. If you do not
588            specify a cluster, the default cluster is assumed..
589
590        :type task_definition: string
591        :param task_definition: The `family` and `revision` (
592            `family:revision`) or full Amazon Resource Name (ARN) of the task
593            definition that you want to start.
594
595        :type overrides: dict
596        :param overrides:
597
598        :type container_instances: list
599        :param container_instances: The container instance UUIDs or full Amazon
600            Resource Name (ARN) entries for the container instances on which
601            you would like to place your task.
602
603        """
604        params = {'taskDefinition': task_definition, }
605        self.build_list_params(params,
606                               container_instances,
607                               'containerInstances.member')
608        if cluster is not None:
609            params['cluster'] = cluster
610        if overrides is not None:
611            params['overrides'] = overrides
612        return self._make_request(
613            action='StartTask',
614            verb='POST',
615            path='/', params=params)
616
617    def stop_task(self, task, cluster=None):
618        """
619        Stops a running task.
620
621        :type cluster: string
622        :param cluster: The short name or full Amazon Resource Name (ARN) of
623            the cluster that hosts the task you want to stop. If you do not
624            specify a cluster, the default cluster is assumed..
625
626        :type task: string
627        :param task: The task UUIDs or full Amazon Resource Name (ARN) entry of
628            the task you would like to stop.
629
630        """
631        params = {'task': task, }
632        if cluster is not None:
633            params['cluster'] = cluster
634        return self._make_request(
635            action='StopTask',
636            verb='POST',
637            path='/', params=params)
638
639    def submit_container_state_change(self, cluster=None, task=None,
640                                      container_name=None, status=None,
641                                      exit_code=None, reason=None,
642                                      network_bindings=None):
643        """
644        This action is only used by the Amazon EC2 Container Service
645        agent, and it is not intended for use outside of the agent.
646
647
648        Sent to acknowledge that a container changed states.
649
650        :type cluster: string
651        :param cluster: The short name or full Amazon Resource Name (ARN) of
652            the cluster that hosts the container.
653
654        :type task: string
655        :param task: The task UUID or full Amazon Resource Name (ARN) of the
656            task that hosts the container.
657
658        :type container_name: string
659        :param container_name: The name of the container.
660
661        :type status: string
662        :param status: The status of the state change request.
663
664        :type exit_code: integer
665        :param exit_code: The exit code returned for the state change request.
666
667        :type reason: string
668        :param reason: The reason for the state change request.
669
670        :type network_bindings: list
671        :param network_bindings: The network bindings of the container.
672
673        """
674        params = {}
675        if cluster is not None:
676            params['cluster'] = cluster
677        if task is not None:
678            params['task'] = task
679        if container_name is not None:
680            params['containerName'] = container_name
681        if status is not None:
682            params['status'] = status
683        if exit_code is not None:
684            params['exitCode'] = exit_code
685        if reason is not None:
686            params['reason'] = reason
687        if network_bindings is not None:
688            self.build_complex_list_params(
689                params, network_bindings,
690                'networkBindings.member',
691                ('bindIP', 'containerPort', 'hostPort'))
692        return self._make_request(
693            action='SubmitContainerStateChange',
694            verb='POST',
695            path='/', params=params)
696
697    def submit_task_state_change(self, cluster=None, task=None, status=None,
698                                 reason=None):
699        """
700        This action is only used by the Amazon EC2 Container Service
701        agent, and it is not intended for use outside of the agent.
702
703
704        Sent to acknowledge that a task changed states.
705
706        :type cluster: string
707        :param cluster: The short name or full Amazon Resource Name (ARN) of
708            the cluster that hosts the task.
709
710        :type task: string
711        :param task: The task UUID or full Amazon Resource Name (ARN) of the
712            task in the state change request.
713
714        :type status: string
715        :param status: The status of the state change request.
716
717        :type reason: string
718        :param reason: The reason for the state change request.
719
720        """
721        params = {}
722        if cluster is not None:
723            params['cluster'] = cluster
724        if task is not None:
725            params['task'] = task
726        if status is not None:
727            params['status'] = status
728        if reason is not None:
729            params['reason'] = reason
730        return self._make_request(
731            action='SubmitTaskStateChange',
732            verb='POST',
733            path='/', params=params)
734
735    def _make_request(self, action, verb, path, params):
736        params['ContentType'] = 'JSON'
737        response = self.make_request(action=action, verb='POST',
738                                     path='/', params=params)
739        body = response.read().decode('utf-8')
740        boto.log.debug(body)
741        if response.status == 200:
742            return json.loads(body)
743        else:
744            json_body = json.loads(body)
745            fault_name = json_body.get('Error', {}).get('Code', None)
746            exception_class = self._faults.get(fault_name, self.ResponseError)
747            raise exception_class(response.status, response.reason,
748                                  body=json_body)
749