diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 5c3fcac1..e0736708 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -515,3 +515,35 @@ def is_valid_cidr(cird): cidr_pattern = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$' cidr_pattern_re = re.compile(cidr_pattern) return cidr_pattern_re.match(cird) is not None + + +def generate_instance_identity_document(instance): + """ + http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html + + A JSON file that describes an instance. Usually retrieved by URL: + http://169.254.169.254/latest/dynamic/instance-identity/document + Here we just fill a dictionary that represents the document + + Typically, this document is used by the amazon-ecs-agent when registering a + new ContainerInstance + """ + + document = { + 'devPayProductCodes': None, + 'availabilityZone': instance.placement['AvailabilityZone'], + 'privateIp': instance.private_ip_address, + 'version': '2010-8-31', + 'region': instance.placement['AvailabilityZone'][:-1], + 'instanceId': instance.id, + 'billingProducts': None, + 'instanceType': instance.instance_type, + 'accountId': '012345678910', + 'pendingTime': '2015-11-19T16:32:11Z', + 'imageId': instance.image_id, + 'kernelId': instance.kernel_id, + 'ramdiskId': instance.ramdisk_id, + 'architecture': instance.architecture, + } + + return document diff --git a/moto/ecs/models.py b/moto/ecs/models.py index d62d7ffa..d4e89b57 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -85,11 +85,48 @@ class Service(BaseObject): return response_object +class ContainerInstance(BaseObject): + def __init__(self, ec2_instance_id): + self.ec2_instance_id = ec2_instance_id + self.status = 'ACTIVE' + self.registeredResources = [] + self.agentConnected = True + self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(str(uuid.uuid1())) + self.pendingTaskCount = 0 + self.remainingResources = [] + self.runningTaskCount = 0 + self.versionInfo = { + 'agentVersion': "1.0.0", + 'agentHash': '4023248', + 'dockerVersion': 'DockerVersion: 1.5.0' + } + + @property + def response_object(self): + response_object = self.gen_response_object() + del response_object['name'], response_object['arn'] + return response_object + + +class ContainerInstanceFailure(BaseObject): + def __init__(self, reason, container_instance_id): + self.reason = reason + self.arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(container_instance_id) + + @property + def response_object(self): + response_object = self.gen_response_object() + response_object['reason'] = self.reason + response_object['arn'] = self.arn + return response_object + + class EC2ContainerServiceBackend(BaseBackend): def __init__(self): self.clusters = {} self.task_definitions = {} self.services = {} + self.container_instances = {} def fetch_task_definition(self, task_definition_str): task_definition_components = task_definition_str.split(':') @@ -212,6 +249,41 @@ class EC2ContainerServiceBackend(BaseBackend): else: raise Exception("cluster {0} or service {1} does not exist".format(cluster_name, service_name)) + def register_container_instance(self, cluster_str, ec2_instance_id): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) + container_instance = ContainerInstance(ec2_instance_id) + if not self.container_instances.get(cluster_name): + self.container_instances[cluster_name] = {} + container_instance_id = container_instance.containerInstanceArn.split('/')[-1] + self.container_instances[cluster_name][container_instance_id] = container_instance + return container_instance + + def list_container_instances(self, cluster_str): + cluster_name = cluster_str.split('/')[-1] + container_instances_values = self.container_instances.get(cluster_name, {}).values() + container_instances = [ci.containerInstanceArn for ci in container_instances_values] + return sorted(container_instances) + + def describe_container_instances(self, cluster_str, list_container_instance_ids): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) + failures = [] + container_instance_objects = [] + for container_instance_id in list_container_instance_ids: + container_instance = self.container_instances[cluster_name].get(container_instance_id, None) + if container_instance is not None: + container_instance_objects.append(container_instance) + else: + failures.append(ContainerInstanceFailure('MISSING', container_instance_id)) + + return container_instance_objects, failures + + def deregister_container_instance(self, cluster_str, container_instance_str): + pass + ecs_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 1cf88bd4..b9770a43 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -113,3 +113,29 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({ 'service': service.response_object }) + + def register_container_instance(self): + cluster_str = self._get_param('cluster') + instance_identity_document_str = self._get_param('instanceIdentityDocument') + instance_identity_document = json.loads(instance_identity_document_str) + ec2_instance_id = instance_identity_document["instanceId"] + container_instance = self.ecs_backend.register_container_instance(cluster_str, ec2_instance_id) + return json.dumps({ + 'containerInstance' : container_instance.response_object + }) + + def list_container_instances(self): + cluster_str = self._get_param('cluster') + container_instance_arns = self.ecs_backend.list_container_instances(cluster_str) + return json.dumps({ + 'containerInstanceArns': container_instance_arns + }) + + def describe_container_instances(self): + cluster_str = self._get_param('cluster') + list_container_instance_arns = self._get_param('containerInstances') + container_instances, failures = self.ecs_backend.describe_container_instances(cluster_str, list_container_instance_arns) + return json.dumps({ + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] + }) diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index e902c5d9..2c6617d7 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -1,8 +1,12 @@ from __future__ import unicode_literals import boto3 import sure # noqa +import json +from moto.ec2 import utils as ec2_utils +from uuid import UUID from moto import mock_ecs +from moto import mock_ec2 @mock_ecs @@ -333,4 +337,119 @@ def test_delete_service(): response['service']['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') response['service']['serviceName'].should.equal('test_ecs_service') response['service']['status'].should.equal('ACTIVE') - response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') \ No newline at end of file + response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + +@mock_ec2 +@mock_ecs +def test_register_container_instance(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + + response['containerInstance']['ec2InstanceId'].should.equal(test_instance.id) + full_arn = response['containerInstance']['containerInstanceArn'] + arn_part = full_arn.split('/') + arn_part[0].should.equal('arn:aws:ecs:us-east-1:012345678910:container-instance') + arn_part[1].should.equal(str(UUID(arn_part[1]))) + response['containerInstance']['status'].should.equal('ACTIVE') + len(response['containerInstance']['registeredResources']).should.equal(0) + len(response['containerInstance']['remainingResources']).should.equal(0) + response['containerInstance']['agentConnected'].should.equal(True) + response['containerInstance']['versionInfo']['agentVersion'].should.equal('1.0.0') + response['containerInstance']['versionInfo']['agentHash'].should.equal('4023248') + response['containerInstance']['versionInfo']['dockerVersion'].should.equal('DockerVersion: 1.5.0') + +@mock_ec2 +@mock_ecs +def test_list_container_instances(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + instance_to_create = 3 + test_instance_arns = [] + for i in range(0, instance_to_create): + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document) + + test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + + response = ecs_client.list_container_instances(cluster=test_cluster_name) + + len(response['containerInstanceArns']).should.equal(instance_to_create) + for arn in test_instance_arns: + response['containerInstanceArns'].should.contain(arn) + + +@mock_ec2 +@mock_ecs +def test_describe_container_instances(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + instance_to_create = 3 + test_instance_arns = [] + for i in range(0, instance_to_create): + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document) + + test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + + test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns)) + response = ecs_client.describe_container_instances(cluster=test_cluster_name, containerInstances=test_instance_ids) + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_arns = [ci['containerInstanceArn'] for ci in response['containerInstances']] + for arn in test_instance_arns: + response_arns.should.contain(arn)