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 = []

Reply via email to