Extend the API ECS driver to support 'services', updated methods to POST. Working on POST signatures for v4, which is required in the ECS API
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/b64d2fa0 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/b64d2fa0 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/b64d2fa0 Branch: refs/heads/trunk Commit: b64d2fa0e03b4d5a59caa302a481edb108494c32 Parents: a1adaae Author: anthony-shaw <anthony.p.s...@gmail.com> Authored: Wed Dec 30 21:19:59 2015 +1100 Committer: anthony-shaw <anthony.p.s...@gmail.com> Committed: Wed Dec 30 21:19:59 2015 +1100 ---------------------------------------------------------------------- libcloud/common/aws.py | 12 ++- libcloud/container/drivers/ecs.py | 137 +++++++++++++++++++++++++++++---- 2 files changed, 132 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/b64d2fa0/libcloud/common/aws.py ---------------------------------------------------------------------- diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py index c0ac9b8..4d7523e 100644 --- a/libcloud/common/aws.py +++ b/libcloud/common/aws.py @@ -26,6 +26,7 @@ except ImportError: from xml.etree import ElementTree as ET from libcloud.common.base import ConnectionUserAndKey, XmlResponse, BaseDriver +from libcloud.common.base import JsonResponse from libcloud.common.types import InvalidCredsError, MalformedResponseError from libcloud.utils.py3 import b, httplib, urlquote from libcloud.utils.xml import findtext, findall @@ -250,8 +251,8 @@ class AWSRequestSignerAlgorithmV4(AWSRequestSigner): def _get_authorization_v4_header(self, params, headers, dt, method='GET', path='/'): - assert method == 'GET', 'AWS Signature V4 not implemented for ' \ - 'other methods than GET' + assert method in ['GET', 'POST'], 'AWS Signature V4 not implemented for ' \ + 'other methods than GET and POST' credentials_scope = self._get_credential_scope(dt=dt) signed_headers = self._get_signed_headers(headers=headers) @@ -368,6 +369,13 @@ class SignedAWSConnection(AWSTokenConnection): return params, headers +class AWSJsonResponse(JsonResponse): + """ + Amazon ECS response class. + ECS API uses JSON unlike the s3, elb drivers + """ + + def _sign(key, msg, hex=False): if hex: return hmac.new(b(key), b(msg), hashlib.sha256).hexdigest() http://git-wip-us.apache.org/repos/asf/libcloud/blob/b64d2fa0/libcloud/container/drivers/ecs.py ---------------------------------------------------------------------- diff --git a/libcloud/container/drivers/ecs.py b/libcloud/container/drivers/ecs.py index 9aa98d3..6485b10 100644 --- a/libcloud/container/drivers/ecs.py +++ b/libcloud/container/drivers/ecs.py @@ -21,34 +21,25 @@ __all__ = [ from libcloud.container.base import (ContainerDriver, Container, ContainerCluster, ContainerImage) from libcloud.container.types import ContainerState -from libcloud.common.base import JsonResponse -from libcloud.common.aws import SignedAWSConnection - +from libcloud.common.aws import SignedAWSConnection, AWSJsonResponse VERSION = '2014-11-13' HOST = 'ecs.%s.amazonaws.com' -ROOT = '/%s/' % (VERSION) +ROOT = '/' TARGET_BASE = 'AmazonEC2ContainerServiceV%s' % (VERSION.replace('-', '')) -class ECSResponse(JsonResponse): - """ - Amazon ECS response class. - ECS API uses JSON unlike the s3, elb drivers - """ - - -class ECSConnection(SignedAWSConnection): +class ECSJsonConnection(SignedAWSConnection): version = VERSION host = HOST - responseCls = ECSResponse + responseCls = AWSJsonResponse service_name = 'ecs' class ElasticContainerDriver(ContainerDriver): name = 'Amazon Elastic Container Service' website = 'https://aws.amazon.com/ecs/details/' - connectionCls = ECSConnection + connectionCls = ECSJsonConnection supports_clusters = False status_map = { 'RUNNING': ContainerState.RUNNING @@ -57,8 +48,12 @@ class ElasticContainerDriver(ContainerDriver): def __init__(self, access_id, secret, region): super(ElasticContainerDriver, self).__init__(access_id, secret) self.region = region + self.region_name = region self.connection.host = HOST % (region) + def _ex_connection_class_kwargs(self): + return {'signature_version': '4'} + def list_clusters(self): """ Get a list of potential locations to deploy clusters into @@ -71,6 +66,7 @@ class ElasticContainerDriver(ContainerDriver): params = {'Action': 'DescribeClusters'} data = self.connection.request( ROOT, + method='POST', headers=self._get_headers(params['Action']) ).object return self._to_clusters(data) @@ -91,6 +87,7 @@ class ElasticContainerDriver(ContainerDriver): request = {'clusterName': name} response = self.connection.request( ROOT, + method='POST', data=request, headers=self._get_headers(params['Action']) ).object @@ -107,6 +104,7 @@ class ElasticContainerDriver(ContainerDriver): request = {'cluster': cluster.id} data = self.connection.request( ROOT, + method='POST', data=request, headers=self._get_headers(params['Action']) ).object @@ -153,6 +151,7 @@ class ElasticContainerDriver(ContainerDriver): request['family'] = image.name list_response = self.connection.request( ROOT, + method='POST', data=request, headers=self._get_headers('ListTasks') ).object @@ -206,6 +205,7 @@ class ElasticContainerDriver(ContainerDriver): data['family'] = name response = self.connection.request( ROOT, + method='POST', data=data, headers=self._get_headers('RegisterTaskDefinition') ).object @@ -264,6 +264,7 @@ class ElasticContainerDriver(ContainerDriver): request = {'task': container.extra['taskArn']} response = self.connection.request( ROOT, + method='POST', data=request, headers=self._get_headers('StopTask') ).object @@ -313,6 +314,7 @@ class ElasticContainerDriver(ContainerDriver): 'taskDefinition': task_arn} response = self.connection.request( ROOT, + method='POST', data=request, headers=self._get_headers('RunTask') ).object @@ -333,6 +335,7 @@ class ElasticContainerDriver(ContainerDriver): describe_request = {'tasks': task_arns} descripe_response = self.connection.request( ROOT, + method='POST', data=describe_request, headers=self._get_headers('DescribeTasks') ).object @@ -342,9 +345,113 @@ class ElasticContainerDriver(ContainerDriver): task, task['taskDefinitionArn'])) return containers + def ex_create_service(self, name, cluster, + task_definition, desired_count=1): + """ + Runs and maintains a desired number of tasks from a specified + task definition. If the number of tasks running in a service + drops below desired_count, Amazon ECS spawns another + instantiation of the task in the specified cluster. + + :param name: the name of the service + :type name: ``str`` + + :param cluster: The cluster to run the service on + :type cluster: :class:`ContainerCluster` + + :param task_definition: The task definition name or ARN for the + service + :type task_definition: ``str`` + + :param desired_count: The desired number of tasks to be running + at any one time + :type desired_count: ``int`` + + :rtype: ``object`` The service object + """ + request = { + 'serviceName': name, + 'taskDefinition': task_definition, + 'desiredCount': desired_count, + 'cluster': cluster.id} + response = self.connection.request( + ROOT, + method='POST', + data=request, + headers=self._get_headers('CreateService') + ).object + return response + + def ex_list_service_arns(self, cluster=None): + """ + List the services + + :param cluster: The cluster hosting the services + :type cluster: :class:`ContainerCluster` + + :rtype: ``list`` of ``str`` + """ + request = {} + if cluster is not None: + request['cluster'] = cluster.id + response = self.connection.request( + ROOT, + method='POST', + data=request, + headers=self._get_headers('ListServices') + ).object + return response['serviceArns'] + + def ex_describe_service(self, cluster, service_arn): + """ + Get the details of a service + + :param cluster: The hosting cluster + :type cluster: :class:`ContainerCluster` + + :param service_arn: The service ARN to describe + :type service_arn: ``str`` + + :return: The service object + :rtype: ``object`` + """ + request = {'services': [service_arn]} + if cluster is not None: + request['cluster'] = cluster.id + response = self.connection.request( + ROOT, + method='POST', + data=request, + headers=self._get_headers('DescribeServices') + ).object + return response['services'][0] + + def ex_destroy_service(self, cluster, service_arn): + """ + Deletes a service + + :param cluster: The target cluster + :type cluster: :class:`ContainerCluster` + + :param service_arn: The service ARN to destroy + :type service_arn: ``str`` + """ + request = { + 'service': service_arn, + 'cluster': cluster.id} + response = self.connection.request( + ROOT, + method='POST', + data=request, + headers=self._get_headers('DeleteService') + ).object + return response + def _get_headers(self, action): return {'x-amz-target': '%s.%s' % - (TARGET_BASE, action)} + (TARGET_BASE, action), + 'Content-Type': 'application/x-amz-json-1.1' + } def _to_clusters(self, data): clusters = []