diff --git a/moto/core/responses.py b/moto/core/responses.py index 5f40abb3..79b0af63 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -105,7 +105,6 @@ class BaseResponse(_TemplateEnvironmentMixin): # FIXME: At least in Flask==0.10.1, request.data is an empty string # and the information we want is in request.form. Keeping self.body # definition for back-compatibility - #if request.headers.get("content-type") == "application/x-amz-json-1.1": self.body = request.data querystring = {} diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index 15d491c3..8a3cdd16 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -49,6 +49,9 @@ class OpsworkInstance(object): # may not be totally accurate defaults; instance-type dependent self.root_device_type = root_device_type + # todo: refactor how we track block_device_mappings to use + # boto.ec2.blockdevicemapping.BlockDeviceType and standardize + # formatting in to_dict() self.block_device_mappings = block_device_mappings if self.block_device_mappings is None: self.block_device_mappings = [{ @@ -83,7 +86,7 @@ class OpsworkInstance(object): start_instance. Update instance attributes to the newly created instance attributes """ - if self.instances is None: + if self.instance is None: reservation = self.ec2_backend.add_instances( image_id=self.ami_id, count=1, @@ -107,6 +110,7 @@ class OpsworkInstance(object): self.architecture = self.instance.architecture self.virtualization_type = self.instance.virtualization_type self.subnet_id = self.instance.subnet_id + self.root_device_type = self.instance.root_device_type self.ec2_backend.start_instances([self.instance.id]) @@ -128,6 +132,7 @@ class OpsworkInstance(object): "Hostname": self.hostname, "InfrastructureClass": self.infrastructure_class, "InstallUpdatesOnBoot": self.install_updates_on_boot, + "InstanceProfileArn": self.instance_profile_arn, "InstanceType": self.instance_type, "LayerIds": self.layer_ids, "Os": self.os, @@ -145,13 +150,16 @@ class OpsworkInstance(object): d.update({"AutoScaleType": self.auto_scale_type}) if self.instance is not None: - del d['AmiId'] d.update({"Ec2InstanceId": self.instance.id}) d.update({"ReportedAgentVersion": "2425-20160406102508 (fixed)"}) d.update({"RootDeviceVolumeId": "vol-a20e450a (fixed)"}) if self.ssh_keyname is not None: d.update({"SshHostDsaKeyFingerprint": "24:36:32:fe:d8:5f:9c:18:b1:ad:37:e9:eb:e8:69:58 (fixed)"}) d.update({"SshHostRsaKeyFingerprint": "3c:bd:37:52:d7:ca:67:e1:6e:4b:ac:31:86:79:f5:6c (fixed)"}) + d.update({"PrivateDns": self.instance.private_dns}) + d.update({"PrivateIp": self.instance.private_ip}) + d.update({"PublicDns": getattr(self.instance, 'public_dns', None)}) + d.update({"PublicIp": getattr(self.instance, 'public_ip', None)}) return d @@ -343,7 +351,7 @@ class Stack(object): # this doesn't match amazon's implementation return "{theme}-{rand}-(moto)".format( theme=self.hostname_theme, - rand=[choice("abcdefghijhk") for _ in xrange(4)]) + rand=[choice("abcdefghijhk") for _ in range(4)]) @property def arn(self): @@ -432,14 +440,16 @@ class OpsWorksBackend(BaseBackend): if unknown_layers: raise ResourceNotFoundException(", ".join(unknown_layers)) - if any([layer.stack_id != stack_id for layer in self.layers.values()]): + layers = [self.layers[id] for id in layer_ids] + if len(set([layer.stack_id for layer in layers])) != 1 or \ + any([layer.stack_id != stack_id for layer in layers]): raise ValidationException( "Please only provide layer IDs from the same stack") stack = self.stacks[stack_id] # pick the first to set default instance_profile_arn and # security_group_ids on the instance. - layer = self.layers[layer_ids[0]] + layer = layers[0] kwargs.setdefault("hostname", stack.generate_hostname()) kwargs.setdefault("ssh_keyname", stack.default_ssh_keyname) @@ -482,6 +492,37 @@ class OpsWorksBackend(BaseBackend): raise ResourceNotFoundException(", ".join(unknown_layers)) return [self.layers[id].to_dict() for id in layer_ids] + def describe_instances(self, instance_ids, layer_id, stack_id): + if len(list(filter(None, (instance_ids, layer_id, stack_id)))) != 1: + raise ValidationException("Please provide either one or more " + "instance IDs or one stack ID or one " + "layer ID") + if instance_ids: + unknown_instances = set(instance_ids) - set(self.instances.keys()) + if unknown_instances: + raise ResourceNotFoundException(", ".join(unknown_instances)) + return [self.instances[id].to_dict() for id in instance_ids] + + if layer_id: + if layer_id not in self.layers: + raise ResourceNotFoundException( + "Unable to find layer with ID {}".format(layer_id)) + instances = [i.to_dict() for i in self.instances.values() if layer_id in i.layer_ids] + return instances + + if stack_id: + if stack_id not in self.stacks: + raise ResourceNotFoundException( + "Unable to find stack with ID {}".format(stack_id)) + instances = [i.to_dict() for i in self.instances.values() if stack_id==i.stack_id] + return instances + + def start_instance(self, instance_id): + if instance_id not in self.instances: + raise ResourceNotFoundException( + "Unable to find instance with ID {}".format(instance_id)) + self.instances[instance_id].start() + opsworks_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py index cd6bfce8..47fed301 100644 --- a/moto/opsworks/responses.py +++ b/moto/opsworks/responses.py @@ -96,5 +96,17 @@ class OpsWorksResponse(BaseResponse): stack_id = self.parameters.get("StackId") layer_ids = self.parameters.get("LayerIds") layers = self.opsworks_backend.describe_layers(stack_id, layer_ids) - return json.dumps({"Layers": layers}) + return json.dumps({"Layers": layers}, indent=1) + def describe_instances(self): + instance_ids = self.parameters.get("InstanceIds") + layer_id = self.parameters.get("LayerId") + stack_id = self.parameters.get("StackId") + instances = self.opsworks_backend.describe_instances( + instance_ids, layer_id, stack_id) + return json.dumps({"Instances": instances}, indent=1) + + def start_instance(self): + instance_id = self.parameters.get("InstanceId") + self.opsworks_backend.start_instance(instance_id) + return "" diff --git a/tests/test_opsworks/test_instances.py b/tests/test_opsworks/test_instances.py index 1050093d..0175eec4 100644 --- a/tests/test_opsworks/test_instances.py +++ b/tests/test_opsworks/test_instances.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals import boto3 import sure # noqa -import re from moto import mock_opsworks +from moto import mock_ec2 + @mock_opsworks -def test_create_instance_response(): +def test_create_instance(): client = boto3.client('opsworks') stack_id = client.create_stack( Name="test_stack_1", @@ -28,3 +29,148 @@ def test_create_instance_response(): response.should.contain("InstanceId") + client.create_instance.when.called_with( + StackId="nothere", LayerIds=[layer_id], InstanceType="t2.micro" + ).should.throw(Exception, "Unable to find stack with ID nothere") + + client.create_instance.when.called_with( + StackId=stack_id, LayerIds=["nothere"], InstanceType="t2.micro" + ).should.throw(Exception, "nothere") + + +@mock_opsworks +def test_describe_instances(): + """ + create two stacks, with 1 layer and 2 layers (S1L1, S2L1, S2L2) + + populate S1L1 with 2 instances (S1L1_i1, S1L1_i2) + populate S2L1 with 1 instance (S2L1_i1) + populate S2L2 with 3 instances (S2L2_i1..2) + """ + + client = boto3.client('opsworks') + S1 = client.create_stack( + Name="S1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + S1L1 = client.create_layer( + StackId=S1, + Type="custom", + Name="S1L1", + Shortname="S1L1" + )['LayerId'] + S2 = client.create_stack( + Name="S2", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + S2L1 = client.create_layer( + StackId=S2, + Type="custom", + Name="S2L1", + Shortname="S2L1" + )['LayerId'] + S2L2 = client.create_layer( + StackId=S2, + Type="custom", + Name="S2L2", + Shortname="S2L2" + )['LayerId'] + + S1L1_i1 = client.create_instance( + StackId=S1, LayerIds=[S1L1], InstanceType="t2.micro" + )['InstanceId'] + S1L1_i2 = client.create_instance( + StackId=S1, LayerIds=[S1L1], InstanceType="t2.micro" + )['InstanceId'] + S2L1_i1 = client.create_instance( + StackId=S2, LayerIds=[S2L1], InstanceType="t2.micro" + )['InstanceId'] + S2L2_i1 = client.create_instance( + StackId=S2, LayerIds=[S2L2], InstanceType="t2.micro" + )['InstanceId'] + S2L2_i2 = client.create_instance( + StackId=S2, LayerIds=[S2L2], InstanceType="t2.micro" + )['InstanceId'] + + # instances in Stack 1 + response = client.describe_instances(StackId=S1)['Instances'] + response.should.have.length_of(2) + S1L1_i1.should.be.within([i["InstanceId"] for i in response]) + S1L1_i2.should.be.within([i["InstanceId"] for i in response]) + + response2 = client.describe_instances(InstanceIds=[S1L1_i1, S1L1_i2])['Instances'] + sorted(response2, key=lambda d: d['InstanceId']).should.equal( + sorted(response, key=lambda d: d['InstanceId'])) + + response3 = client.describe_instances(LayerId=S1L1)['Instances'] + sorted(response3, key=lambda d: d['InstanceId']).should.equal( + sorted(response, key=lambda d: d['InstanceId'])) + + response = client.describe_instances(StackId=S1)['Instances'] + response.should.have.length_of(2) + S1L1_i1.should.be.within([i["InstanceId"] for i in response]) + S1L1_i2.should.be.within([i["InstanceId"] for i in response]) + + # instances in Stack 2 + response = client.describe_instances(StackId=S2)['Instances'] + response.should.have.length_of(3) + S2L1_i1.should.be.within([i["InstanceId"] for i in response]) + S2L2_i1.should.be.within([i["InstanceId"] for i in response]) + S2L2_i2.should.be.within([i["InstanceId"] for i in response]) + + response = client.describe_instances(LayerId=S2L1)['Instances'] + response.should.have.length_of(1) + S2L1_i1.should.be.within([i["InstanceId"] for i in response]) + + response = client.describe_instances(LayerId=S2L2)['Instances'] + response.should.have.length_of(2) + S2L1_i1.should_not.be.within([i["InstanceId"] for i in response]) + + +@mock_opsworks +@mock_ec2 +def test_ec2_integration(): + """ + instances created via OpsWorks should be discoverable via ec2 + """ + + opsworks = boto3.client('opsworks') + stack_id = opsworks.create_stack( + Name="S1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + + layer_id = opsworks.create_layer( + StackId=stack_id, + Type="custom", + Name="S1L1", + Shortname="S1L1" + )['LayerId'] + + instance_id = opsworks.create_instance( + StackId=stack_id, LayerIds=[layer_id], InstanceType="t2.micro" + )['InstanceId'] + + ec2 = boto3.client('ec2') + + # Before starting the instance, it shouldn't be discoverable via ec2 + reservations = ec2.describe_instances()['Reservations'] + assert reservations.should.be.empty + + # After starting the instance, it should be discoverable via ec2 + opsworks.start_instance(InstanceId=instance_id) + reservations = ec2.describe_instances()['Reservations'] + reservations[0]['Instances'].should.have.length_of(1) + instance = reservations[0]['Instances'][0] + opsworks_instance = opsworks.describe_instances(StackId=stack_id)['Instances'][0] + + instance['InstanceId'].should.equal(opsworks_instance['Ec2InstanceId']) + instance['PrivateIpAddress'].should.equal(opsworks_instance['PrivateIp']) + + diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py index 8e17c941..8bb682ca 100644 --- a/tests/test_opsworks/test_stack.py +++ b/tests/test_opsworks/test_stack.py @@ -21,7 +21,7 @@ def test_create_stack_response(): @mock_opsworks def test_describe_stacks(): client = boto3.client('opsworks') - for i in xrange(1, 4): + for i in range(1, 4): client.create_stack( Name="test_stack_{}".format(i), Region="us-east-1",