From 165bab0f97e0e6e8a039bfb82316f3f130cd60aa Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Tue, 12 Apr 2016 16:23:35 -0400 Subject: [PATCH 1/7] opsworks: implement create_stack --- moto/__init__.py | 1 + moto/backends.py | 2 + moto/core/responses.py | 1 + moto/opsworks/__init__.py | 13 +++ moto/opsworks/exceptions.py | 14 +++ moto/opsworks/models.py | 168 ++++++++++++++++++++++++++++++ moto/opsworks/responses.py | 47 +++++++++ moto/opsworks/urls.py | 12 +++ tests/test_opsworks/test_stack.py | 41 ++++++++ 9 files changed, 299 insertions(+) create mode 100644 moto/opsworks/__init__.py create mode 100644 moto/opsworks/exceptions.py create mode 100644 moto/opsworks/models.py create mode 100644 moto/opsworks/responses.py create mode 100644 moto/opsworks/urls.py create mode 100644 tests/test_opsworks/test_stack.py diff --git a/moto/__init__.py b/moto/__init__.py index d497eec0..ec39560b 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -18,6 +18,7 @@ from .ecs import mock_ecs # flake8: noqa from .elb import mock_elb # flake8: noqa from .emr import mock_emr # flake8: noqa from .glacier import mock_glacier # flake8: noqa +from .opsworks import mock_opsworks # flake8: noqa from .iam import mock_iam # flake8: noqa from .kinesis import mock_kinesis # flake8: noqa from .kms import mock_kms # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index b66af577..d1262a7c 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -12,6 +12,7 @@ from moto.elb import elb_backend from moto.emr import emr_backend from moto.glacier import glacier_backend from moto.iam import iam_backend +from moto.opsworks import opsworks_backend from moto.kinesis import kinesis_backend from moto.kms import kms_backend from moto.rds import rds_backend @@ -36,6 +37,7 @@ BACKENDS = { 'emr': emr_backend, 'glacier': glacier_backend, 'iam': iam_backend, + 'opsworks': opsworks_backend, 'kinesis': kinesis_backend, 'kms': kms_backend, 'redshift': redshift_backend, diff --git a/moto/core/responses.py b/moto/core/responses.py index 79b0af63..5f40abb3 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -105,6 +105,7 @@ 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/__init__.py b/moto/opsworks/__init__.py new file mode 100644 index 00000000..dfcd582e --- /dev/null +++ b/moto/opsworks/__init__.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from .models import opsworks_backends +from ..core.models import MockAWS + +opsworks_backend = opsworks_backends['us-east-1'] + + +def mock_opsworks(func=None): + if func: + return MockAWS(opsworks_backends)(func) + else: + return MockAWS(opsworks_backends) + diff --git a/moto/opsworks/exceptions.py b/moto/opsworks/exceptions.py new file mode 100644 index 00000000..e41cc7a3 --- /dev/null +++ b/moto/opsworks/exceptions.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class ResourceNotFoundException(BadRequest): + def __init__(self, message): + super(ResourceNotFoundError, self).__init__() + self.description = json.dumps({ + "message": message, + '__type': 'ResourceNotFoundException', + }) + diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py new file mode 100644 index 00000000..34114b4f --- /dev/null +++ b/moto/opsworks/models.py @@ -0,0 +1,168 @@ +from __future__ import unicode_literals +from moto.core import BaseBackend +from moto.ec2 import ec2_backends +from moto.elb import elb_backends +import uuid +import datetime + +from .exceptions import ResourceNotFoundException + + +class Layer(object): + def __init__(self, stack_id, type, name, shortname, attributes, + custom_instance_profile_arn, custom_json, + custom_security_group_ids, packages, volume_configurations, + enable_autohealing, auto_assign_elastic_ips, + auto_assign_public_ips, custom_recipes, install_updates_on_boot, + use_ebs_optimized_instances, lifecycle_event_configuration): + self.stack_id = stack_id + self.type = type + self.name = name + self.shortname = shortname + self.attributes = attributes + self.custom_instance_profile_arn = custom_instance_profile_arn + self.custom_json = custom_json + self.custom_security_group_ids = custom_security_group_ids + self.packages = packages + self.volume_configurations = volume_configurations + self.enable_autohealing = enable_autohealing + self.auto_assign_elastic_ips = auto_assign_elastic_ips + self.auto_assign_public_ips = auto_assign_public_ips + self.custom_recipes = custom_recipes + self.install_updates_on_boot = install_updates_on_boot + self.use_ebs_optimized_instances = use_ebs_optimized_instances + self.lifecycle_event_configuration = lifecycle_event_configuration + self.instances = [] + + +class Stack(object): + def __init__(self, name, region, service_role_arn, default_instance_profile_arn, + vpcid='vpc-1f99bf7c', + attributes=None, + default_os='Ubuntu 12.04 LTS', + hostname_theme='Layer_Dependent', + default_availability_zone='us-east-1a', + default_subnet_id='subnet-73981004', + custom_json=None, + configuration_manager=None, + chef_configuration=None, + use_custom_cookbooks=False, + use_opsworks_security_groups=True, + custom_cookbooks_source=None, + default_ssh_keyname=None, + default_root_device_type='instance-store', + agent_version='LATEST'): + + self.name = name + self.region = region + self.service_role_arn = service_role_arn + self.default_instance_profile_arn = default_instance_profile_arn + + self.vpcid = vpcid + self.attributes = attributes + if attributes is None: + self.attributes = {'Color': None} + + self.configuration_manager = configuration_manager + if configuration_manager is None: + self.configuration_manager = {'Name': 'Chef', 'Version': '11.4'} + + self.chef_configuration = chef_configuration + if chef_configuration is None: + self.chef_configuration = {} + + self.custom_cookbooks_source = custom_cookbooks_source + if custom_cookbooks_source is None: + self.custom_cookbooks_source = {} + + self.custom_json = custom_json + self.default_ssh_keyname = default_ssh_keyname + self.default_os = default_os + self.hostname_theme = hostname_theme + self.default_availability_zone = default_availability_zone + self.default_subnet_id = default_subnet_id + self.use_custom_cookbooks = use_custom_cookbooks + self.use_opsworks_security_groups = use_opsworks_security_groups + self.default_root_device_type = default_root_device_type + self.agent_version = agent_version + + self.id = "{}".format(uuid.uuid4()) + self.layers = [] + self.apps = [] + self.account_number = "123456789012" + self.created_at = datetime.datetime.utcnow() + + def __eq__(self, other): + return self.id == other.id + + @property + def arn(self): + return "arn:aws:opsworks:{region}:{account_number}:stack/{id}".format( + region=self.region, + account_number=self.account_number, + id=self.id + ) + + def to_dict(self): + response = { + "AgentVersion": self.agent_version, + "Arn": self.arn, + "Attributes": self.attributes, + "ChefConfiguration": self.chef_configuration, + "ConfigurationManager": self.configuration_manager, + "CreatedAt": self.created_at.isoformat(), + "CustomCookbooksSource": self.custom_cookbooks_source, + "DefaultAvailabilityZone": self.default_availability_zone, + "DefaultInstanceProfileArn": self.default_instance_profile_arn, + "DefaultOs": self.default_os, + "DefaultRootDeviceType": self.default_root_device_type, + "DefaultSshKeyName": self.default_ssh_keyname, + "DefaultSubnetId": self.default_subnet_id, + "HostnameTheme": self.hostname_theme, + "Name": self.name, + "Region": self.region, + "ServiceRoleArn": self.service_role_arn, + "StackId": self.id, + "UseCustomCookbooks": self.use_custom_cookbooks, + "UseOpsworksSecurityGroups": self.use_opsworks_security_groups, + "VpcId": self.vpcid + } + if self.custom_json is not None: + response.update({"CustomJson": self.custom_json}) + if self.default_ssh_keyname is not None: + response.update({"DefaultSshKeyName": self.default_ssh_keyname}) + return response + + +class OpsWorksBackend(BaseBackend): + def __init__(self, ec2_backend, elb_backend): + self.stacks = {} + self.layers = {} + self.instances = {} + self.policies = {} + self.ec2_backend = ec2_backend + self.elb_backend = elb_backend + + def reset(self): + ec2_backend = self.ec2_backend + elb_backend = self.elb_backend + self.__dict__ = {} + self.__init__(ec2_backend, elb_backend) + + def create_stack(self, **kwargs): + stack = Stack(**kwargs) + self.stacks[stack.id] = stack + return stack + + def describe_stacks(self, stack_ids=None): + if stack_ids is None: + return [stack.to_dict() for stack in self.stacks.values()] + + unknown_stacks = set(stack_ids) - set(self.stacks.keys()) + if unknown_stacks: + raise ResourceNotFoundException(unknown_stacks) + return [self.stacks[id].to_dict() for id in stack_ids] + +opsworks_backends = {} +for region, ec2_backend in ec2_backends.items(): + opsworks_backends[region] = OpsWorksBackend(ec2_backend, elb_backends[region]) diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py new file mode 100644 index 00000000..9b5d7f15 --- /dev/null +++ b/moto/opsworks/responses.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +import json + +from moto.core.responses import BaseResponse +from .models import opsworks_backends + + +class OpsWorksResponse(BaseResponse): + + @property + def parameters(self): + return json.loads(self.body.decode("utf-8")) + + @property + def opsworks_backend(self): + return opsworks_backends[self.region] + + def create_stack(self): + kwargs = dict( + name=self.parameters.get("Name"), + region=self.parameters.get("Region"), + vpcid=self.parameters.get("VpcId"), + attributes=self.parameters.get("Attributes"), + default_instance_profile_arn=self.parameters.get("DefaultInstanceProfileArn"), + default_os=self.parameters.get("DefaultOs"), + hostname_theme=self.parameters.get("HostnameTheme"), + default_availability_zone=self.parameters.get("DefaultAvailabilityZone"), + default_subnet_id=self.parameters.get("DefaultInstanceProfileArn"), + custom_json=self.parameters.get("CustomJson"), + configuration_manager=self.parameters.get("ConfigurationManager"), + chef_configuration=self.parameters.get("ChefConfiguration"), + use_custom_cookbooks=self.parameters.get("UseCustomCookbooks"), + use_opsworks_security_groups=self.parameters.get("UseOpsworksSecurityGroups"), + custom_cookbooks_source=self.parameters.get("CustomCookbooksSource"), + default_ssh_keyname=self.parameters.get("DefaultSshKeyName"), + default_root_device_type=self.parameters.get("DefaultRootDeviceType"), + service_role_arn=self.parameters.get("ServiceRoleArn"), + agent_version=self.parameters.get("AgentVersion"), + ) + stack = self.opsworks_backend.create_stack(**kwargs) + return json.dumps({"StackId": stack.id}, indent=1) + + def describe_stacks(self): + stack_ids = self.parameters.get("StackIds") + stacks = self.opsworks_backend.describe_stacks(stack_ids) + return json.dumps({"Stacks": stacks}, indent=1) diff --git a/moto/opsworks/urls.py b/moto/opsworks/urls.py new file mode 100644 index 00000000..6913de6b --- /dev/null +++ b/moto/opsworks/urls.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .responses import OpsWorksResponse + +# AWS OpsWorks has a single endpoint: opsworks.us-east-1.amazonaws.com +# and only supports HTTPS requests. +url_bases = [ + "opsworks.us-east-1.amazonaws.com" +] + +url_paths = { + '{0}/$': OpsWorksResponse.dispatch, +} diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py new file mode 100644 index 00000000..1555fa18 --- /dev/null +++ b/tests/test_opsworks/test_stack.py @@ -0,0 +1,41 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa + +from moto import mock_opsworks, mock_ec2, mock_elb + + +@mock_opsworks +def test_create_stack_response(): + client = boto3.client('opsworks') + response = client.create_stack( + Name="test_stack_1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + ) + response.should.contain("StackId") + + +@mock_opsworks +def test_describe_stacks(): + client = boto3.client('opsworks') + for i in xrange(1, 4): + client.create_stack( + Name="test_stack_{}".format(i), + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + ) + + response = client.describe_stacks() + response['Stacks'].should.have.length_of(3) + response['Stacks'][0]['ServiceRoleArn'].should.equal("service_arn") + response['Stacks'][0]['DefaultInstanceProfileArn'].should.equal("profile_arn") + + _id = response['Stacks'][0]['StackId'] + response = client.describe_stacks(StackIds=[_id]) + response['Stacks'].should.have.length_of(1) + response['Stacks'][0]['Arn'].should.contain(_id) + + From 2fe5b77861eae6dec12b6365be27e7ab1b7cbfff Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Thu, 14 Apr 2016 16:28:53 -0400 Subject: [PATCH 2/7] opsworks: impl create_layers; describe_layers --- moto/opsworks/exceptions.py | 10 +- moto/opsworks/models.py | 163 ++++++++++++++++++++++++++--- moto/opsworks/responses.py | 29 +++++ tests/test_opsworks/test_layers.py | 69 ++++++++++++ tests/test_opsworks/test_stack.py | 13 ++- 5 files changed, 266 insertions(+), 18 deletions(-) create mode 100644 tests/test_opsworks/test_layers.py diff --git a/moto/opsworks/exceptions.py b/moto/opsworks/exceptions.py index e41cc7a3..b408b82f 100644 --- a/moto/opsworks/exceptions.py +++ b/moto/opsworks/exceptions.py @@ -6,9 +6,17 @@ from werkzeug.exceptions import BadRequest class ResourceNotFoundException(BadRequest): def __init__(self, message): - super(ResourceNotFoundError, self).__init__() + super(ResourceNotFoundException, self).__init__() self.description = json.dumps({ "message": message, '__type': 'ResourceNotFoundException', }) + +class ValidationException(BadRequest): + def __init__(self, message): + super(ValidationException, self).__init__() + self.description = json.dumps({ + "message": message, + '__type': 'ResourceNotFoundException', + }) diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index 34114b4f..af506bd2 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -5,34 +5,132 @@ from moto.elb import elb_backends import uuid import datetime -from .exceptions import ResourceNotFoundException +from .exceptions import ResourceNotFoundException, ValidationException class Layer(object): - def __init__(self, stack_id, type, name, shortname, attributes, - custom_instance_profile_arn, custom_json, - custom_security_group_ids, packages, volume_configurations, - enable_autohealing, auto_assign_elastic_ips, - auto_assign_public_ips, custom_recipes, install_updates_on_boot, - use_ebs_optimized_instances, lifecycle_event_configuration): + def __init__(self, stack_id, type, name, shortname, + attributes=None, + custom_instance_profile_arn=None, + custom_json=None, + custom_security_group_ids=None, + packages=None, + volume_configurations=None, + enable_autohealing=None, + auto_assign_elastic_ips=None, + auto_assign_public_ips=None, + custom_recipes=None, + install_updates_on_boot=None, + use_ebs_optimized_instances=None, + lifecycle_event_configuration=None): self.stack_id = stack_id self.type = type self.name = name self.shortname = shortname + self.attributes = attributes + if attributes is None: + self.attributes = { + 'BundlerVersion': None, + 'EcsClusterArn': None, + 'EnableHaproxyStats': None, + 'GangliaPassword': None, + 'GangliaUrl': None, + 'GangliaUser': None, + 'HaproxyHealthCheckMethod': None, + 'HaproxyHealthCheckUrl': None, + 'HaproxyStatsPassword': None, + 'HaproxyStatsUrl': None, + 'HaproxyStatsUser': None, + 'JavaAppServer': None, + 'JavaAppServerVersion': None, + 'Jvm': None, + 'JvmOptions': None, + 'JvmVersion': None, + 'ManageBundler': None, + 'MemcachedMemory': None, + 'MysqlRootPassword': None, + 'MysqlRootPasswordUbiquitous': None, + 'NodejsVersion': None, + 'PassengerVersion': None, + 'RailsStack': None, + 'RubyVersion': None, + 'RubygemsVersion': None + } # May not be accurate + + self.packages = packages + if packages is None: + self.packages = packages + + self.custom_recipes = custom_recipes + if custom_recipes is None: + self.custom_recipes = { + 'Configure': [], + 'Deploy': [], + 'Setup': [], + 'Shutdown': [], + 'Undeploy': [], + } + + self.custom_security_group_ids = custom_security_group_ids + if custom_security_group_ids is None: + self.custom_security_group_ids = [] + + self.lifecycle_event_configuration = lifecycle_event_configuration + if lifecycle_event_configuration is None: + self.lifecycle_event_configuration = { + "Shutdown": {"DelayUntilElbConnectionsDrained": False} + } + + self.volume_configurations = volume_configurations + if volume_configurations is None: + self.volume_configurations = [] + self.custom_instance_profile_arn = custom_instance_profile_arn self.custom_json = custom_json - self.custom_security_group_ids = custom_security_group_ids - self.packages = packages - self.volume_configurations = volume_configurations self.enable_autohealing = enable_autohealing self.auto_assign_elastic_ips = auto_assign_elastic_ips self.auto_assign_public_ips = auto_assign_public_ips - self.custom_recipes = custom_recipes self.install_updates_on_boot = install_updates_on_boot self.use_ebs_optimized_instances = use_ebs_optimized_instances - self.lifecycle_event_configuration = lifecycle_event_configuration + self.instances = [] + self.id = "{}".format(uuid.uuid4()) + self.created_at = datetime.datetime.utcnow() + + def __eq__(self, other): + return self.id == other.id + + def to_dict(self): + d = { + "Attributes": self.attributes, + "AutoAssignElasticIps": self.auto_assign_elastic_ips, + "AutoAssignPublicIps": self.auto_assign_public_ips, + "CreatedAt": self.created_at.isoformat(), + "CustomRecipes": self.custom_recipes, + "CustomSecurityGroupIds": self.custom_security_group_ids, + "DefaultRecipes": { + "Configure": [], + "Setup": [], + "Shutdown": [], + "Undeploy": [] + }, # May not be accurate + "DefaultSecurityGroupNames": ['AWS-OpsWorks-Custom-Server'], + "EnableAutoHealing": self.enable_autohealing, + "LayerId": self.id, + "LifecycleEventConfiguration": self.lifecycle_event_configuration, + "Name": self.name, + "Shortname": self.shortname, + "StackId": self.stack_id, + "Type": self.type, + "UseEbsOptimizedInstances": self.use_ebs_optimized_instances, + "VolumeConfigurations": self.volume_configurations, + } + if self.custom_json is not None: + d.update({"CustomJson": self.custom_json}) + if self.custom_instance_profile_arn is not None: + d.update({"CustomInstanceProfileArn": self.custom_instance_profile_arn}) + return d class Stack(object): @@ -154,15 +252,52 @@ class OpsWorksBackend(BaseBackend): self.stacks[stack.id] = stack return stack - def describe_stacks(self, stack_ids=None): + def create_layer(self, **kwargs): + name = kwargs['name'] + shortname = kwargs['shortname'] + stackid = kwargs['stack_id'] + if stackid not in self.stacks: + raise ResourceNotFoundException(stackid) + if name in [l.name for l in self.layers.values()]: + raise ValidationException( + 'There is already a layer named "{}" ' + 'for this stack'.format(name)) + if shortname in [l.shortname for l in self.layers.values()]: + raise ValidationException( + 'There is already a layer with shortname "{}" ' + 'for this stack'.format(shortname)) + layer = Layer(**kwargs) + self.layers[layer.id] = layer + self.stacks[stackid].layers.append(layer) + return layer + + def describe_stacks(self, stack_ids): if stack_ids is None: return [stack.to_dict() for stack in self.stacks.values()] unknown_stacks = set(stack_ids) - set(self.stacks.keys()) if unknown_stacks: - raise ResourceNotFoundException(unknown_stacks) + raise ResourceNotFoundException(", ".join(unknown_stacks)) return [self.stacks[id].to_dict() for id in stack_ids] + def describe_layers(self, stack_id, layer_ids): + if stack_id is not None and layer_ids is not None: + raise ValidationException( + "Please provide one or more layer IDs or a stack ID" + ) + if stack_id is not None: + if stack_id not in self.stacks: + raise ResourceNotFoundException( + "Unable to find stack with ID {}".format(stack_id)) + return [layer.to_dict() for layer in self.stacks[stack_id].layers] + + unknown_layers = set(layer_ids) - set(self.layers.keys()) + if unknown_layers: + raise ResourceNotFoundException(", ".join(unknown_layers)) + return [self.layers[id].to_dict() for id in layer_ids] + + + opsworks_backends = {} for region, ec2_backend in ec2_backends.items(): opsworks_backends[region] = OpsWorksBackend(ec2_backend, elb_backends[region]) diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py index 9b5d7f15..01600b91 100644 --- a/moto/opsworks/responses.py +++ b/moto/opsworks/responses.py @@ -41,7 +41,36 @@ class OpsWorksResponse(BaseResponse): stack = self.opsworks_backend.create_stack(**kwargs) return json.dumps({"StackId": stack.id}, indent=1) + def create_layer(self): + kwargs = dict( + stack_id=self.parameters.get('StackId'), + type=self.parameters.get('Type'), + name=self.parameters.get('Name'), + shortname=self.parameters.get('Shortname'), + attributes=self.parameters.get('Attributes'), + custom_instance_profile_arn=self.parameters.get("CustomInstanceProfileArn"), + custom_json=self.parameters.get("CustomJson"), + custom_security_group_ids=self.parameters.get('CustomSecurityGroupIds'), + packages=self.parameters.get('Packages'), + volume_configurations=self.parameters.get("VolumeConfigurations"), + enable_autohealing=self.parameters.get("EnableAutoHealing"), + auto_assign_elastic_ips=self.parameters.get("AutoAssignElasticIps"), + auto_assign_public_ips=self.parameters.get("AutoAssignPublicIps"), + custom_recipes=self.parameters.get("CustomRecipes"), + install_updates_on_boot=self.parameters.get("InstallUpdatesOnBoot"), + use_ebs_optimized_instances=self.parameters.get("UseEbsOptimizedInstances"), + lifecycle_event_configuration=self.parameters.get("LifecycleEventConfiguration") + ) + layer = self.opsworks_backend.create_layer(**kwargs) + return json.dumps({"LayerId": layer.id}, indent=1) + def describe_stacks(self): stack_ids = self.parameters.get("StackIds") stacks = self.opsworks_backend.describe_stacks(stack_ids) return json.dumps({"Stacks": stacks}, indent=1) + + def describe_layers(self): + 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}, indent=1) diff --git a/tests/test_opsworks/test_layers.py b/tests/test_opsworks/test_layers.py new file mode 100644 index 00000000..128a846f --- /dev/null +++ b/tests/test_opsworks/test_layers.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa +import re + +from moto import mock_opsworks + + +@mock_opsworks +def test_create_layer_response(): + client = boto3.client('opsworks') + stack_id = client.create_stack( + Name="test_stack_1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + + response = client.create_layer( + StackId=stack_id, + Type="custom", + Name="TestLayer", + Shortname="TestLayerShortName" + ) + + response.should.contain("LayerId") + + # ClientError + client.create_layer.when.called_with( + StackId=stack_id, + Type="custom", + Name="TestLayer", + Shortname="_" + ).should.throw( + Exception, re.compile(r'already a layer named "TestLayer"') + ) + # ClientError + client.create_layer.when.called_with( + StackId=stack_id, + Type="custom", + Name="_", + Shortname="TestLayerShortName" + ).should.throw( + Exception, re.compile(r'already a layer with shortname "TestLayerShortName"') + ) + + +@mock_opsworks +def test_describe_layers(): + client = boto3.client('opsworks') + stack_id = client.create_stack( + Name="test_stack_1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + layer_id = client.create_layer( + StackId=stack_id, + Type="custom", + Name="TestLayer", + Shortname="TestLayerShortName" + )['LayerId'] + + rv1 = client.describe_layers(StackId=stack_id) + rv2 = client.describe_layers(LayerIds=[layer_id]) + rv1.should.equal(rv2) + + rv1['Layers'][0]['Name'].should.equal("TestLayer") + diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py index 1555fa18..8e17c941 100644 --- a/tests/test_opsworks/test_stack.py +++ b/tests/test_opsworks/test_stack.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals import boto3 import sure # noqa +import re -from moto import mock_opsworks, mock_ec2, mock_elb +from moto import mock_opsworks @mock_opsworks @@ -30,12 +31,18 @@ def test_describe_stacks(): response = client.describe_stacks() response['Stacks'].should.have.length_of(3) - response['Stacks'][0]['ServiceRoleArn'].should.equal("service_arn") - response['Stacks'][0]['DefaultInstanceProfileArn'].should.equal("profile_arn") + for stack in response['Stacks']: + stack['ServiceRoleArn'].should.equal("service_arn") + stack['DefaultInstanceProfileArn'].should.equal("profile_arn") _id = response['Stacks'][0]['StackId'] response = client.describe_stacks(StackIds=[_id]) response['Stacks'].should.have.length_of(1) response['Stacks'][0]['Arn'].should.contain(_id) + # ClientError/ResourceNotFoundException + client.describe_stacks.when.called_with(StackIds=["foo"]).should.throw( + Exception, re.compile(r'foo') + ) + From 09ca1b6e0c61c451764106ad1b0ab619bfc99f4e Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Fri, 15 Apr 2016 15:44:38 -0400 Subject: [PATCH 3/7] opsworks: impl create_instance --- moto/opsworks/models.py | 205 ++++++++++++++++++++++++-- moto/opsworks/responses.py | 26 +++- tests/test_opsworks/test_instances.py | 30 ++++ 3 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 tests/test_opsworks/test_instances.py diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index af506bd2..15d491c3 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -1,13 +1,160 @@ from __future__ import unicode_literals from moto.core import BaseBackend from moto.ec2 import ec2_backends -from moto.elb import elb_backends import uuid import datetime +from random import choice from .exceptions import ResourceNotFoundException, ValidationException +class OpsworkInstance(object): + """ + opsworks maintains its own set of ec2 instance metadata. + This metadata exists before any instance reservations are made, and is + used to populate a reservation request when "start" is called + """ + def __init__(self, stack_id, layer_ids, instance_type, ec2_backend, + auto_scale_type=None, + hostname=None, + os=None, + ami_id="ami-08111162", + ssh_keyname=None, + availability_zone=None, + virtualization_type="hvm", + subnet_id=None, + architecture="x86_64", + root_device_type="ebs", + block_device_mappings=None, + install_updates_on_boot=True, + ebs_optimized=False, + agent_version="INHERIT", + instance_profile_arn=None, + associate_public_ip=None, + security_group_ids=None): + + self.ec2_backend = ec2_backend + + self.instance_profile_arn = instance_profile_arn + self.agent_version = agent_version + self.ebs_optimized = ebs_optimized + self.install_updates_on_boot = install_updates_on_boot + self.architecture = architecture + self.virtualization_type = virtualization_type + self.ami_id = ami_id + self.auto_scale_type = auto_scale_type + self.instance_type = instance_type + self.layer_ids = layer_ids + self.stack_id = stack_id + + # may not be totally accurate defaults; instance-type dependent + self.root_device_type = root_device_type + self.block_device_mappings = block_device_mappings + if self.block_device_mappings is None: + self.block_device_mappings = [{ + 'DeviceName': 'ROOT_DEVICE', + 'Ebs': { + 'VolumeSize': 8, + 'VolumeType': 'gp2' + } + }] + self.security_group_ids = security_group_ids + if self.security_group_ids is None: + self.security_group_ids = [] + + self.os = os + self.hostname = hostname + self.ssh_keyname = ssh_keyname + self.availability_zone = availability_zone + self.subnet_id = subnet_id + self.associate_public_ip = associate_public_ip + + self.instance = None + self.reported_os = {} + self.infrastructure_class = "ec2 (fixed)" + self.platform = "linux (fixed)" + + self.id = "{}".format(uuid.uuid4()) + self.created_at = datetime.datetime.utcnow() + + def start(self): + """ + create an ec2 reservation if one doesn't already exist and call + start_instance. Update instance attributes to the newly created instance + attributes + """ + if self.instances is None: + reservation = self.ec2_backend.add_instances( + image_id=self.ami_id, + count=1, + user_data="", + security_group_names=[], + security_group_ids=self.security_group_ids, + instance_type=self.instance_type, + key_name=self.ssh_keyname, + ebs_optimized=self.ebs_optimized, + subnet_id=self.subnet_id, + associate_public_ip=self.associate_public_ip, + ) + self.instance = reservation.instances[0] + self.reported_os = { + 'Family': 'rhel (fixed)', + 'Name': 'amazon (fixed)', + 'Version': '2016.03 (fixed)' + } + self.platform = self.instance.platform + self.security_group_ids = self.instance.security_groups + self.architecture = self.instance.architecture + self.virtualization_type = self.instance.virtualization_type + self.subnet_id = self.instance.subnet_id + + self.ec2_backend.start_instances([self.instance.id]) + + @property + def status(self): + if self.instance is None: + return "stopped" + return self.instance._state.name + + def to_dict(self): + d = { + "AgentVersion": self.agent_version, + "Architecture": self.architecture, + "AvailabilityZone": self.availability_zone, + "BlockDeviceMappings": self.block_device_mappings, + "CreatedAt": self.created_at.isoformat(), + "EbsOptimized": self.ebs_optimized, + "InstanceId": self.id, + "Hostname": self.hostname, + "InfrastructureClass": self.infrastructure_class, + "InstallUpdatesOnBoot": self.install_updates_on_boot, + "InstanceType": self.instance_type, + "LayerIds": self.layer_ids, + "Os": self.os, + "Platform": self.platform, + "ReportedOs": self.reported_os, + "RootDeviceType": self.root_device_type, + "SecurityGroupIds": self.security_group_ids, + "AmiId": self.ami_id, + "Status": self.status, + } + if self.ssh_keyname is not None: + d.update({"SshKeyName": self.ssh_keyname}) + + if self.auto_scale_type is not None: + 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)"}) + return d + + class Layer(object): def __init__(self, stack_id, type, name, shortname, attributes=None, @@ -94,7 +241,6 @@ class Layer(object): self.install_updates_on_boot = install_updates_on_boot self.use_ebs_optimized_instances = use_ebs_optimized_instances - self.instances = [] self.id = "{}".format(uuid.uuid4()) self.created_at = datetime.datetime.utcnow() @@ -135,7 +281,7 @@ class Layer(object): class Stack(object): def __init__(self, name, region, service_role_arn, default_instance_profile_arn, - vpcid='vpc-1f99bf7c', + vpcid="vpc-1f99bf7a", attributes=None, default_os='Ubuntu 12.04 LTS', hostname_theme='Layer_Dependent', @@ -193,6 +339,12 @@ class Stack(object): def __eq__(self, other): return self.id == other.id + def generate_hostname(self): + # this doesn't match amazon's implementation + return "{theme}-{rand}-(moto)".format( + theme=self.hostname_theme, + rand=[choice("abcdefghijhk") for _ in xrange(4)]) + @property def arn(self): return "arn:aws:opsworks:{region}:{account_number}:stack/{id}".format( @@ -233,19 +385,16 @@ class Stack(object): class OpsWorksBackend(BaseBackend): - def __init__(self, ec2_backend, elb_backend): + def __init__(self, ec2_backend): self.stacks = {} self.layers = {} self.instances = {} - self.policies = {} self.ec2_backend = ec2_backend - self.elb_backend = elb_backend def reset(self): ec2_backend = self.ec2_backend - elb_backend = self.elb_backend self.__dict__ = {} - self.__init__(ec2_backend, elb_backend) + self.__init__(ec2_backend) def create_stack(self, **kwargs): stack = Stack(**kwargs) @@ -271,6 +420,43 @@ class OpsWorksBackend(BaseBackend): self.stacks[stackid].layers.append(layer) return layer + def create_instance(self, **kwargs): + stack_id = kwargs['stack_id'] + layer_ids = kwargs['layer_ids'] + + if stack_id not in self.stacks: + raise ResourceNotFoundException( + "Unable to find stack with ID {}".format(stack_id)) + + unknown_layers = set(layer_ids) - set(self.layers.keys()) + if unknown_layers: + raise ResourceNotFoundException(", ".join(unknown_layers)) + + if any([layer.stack_id != stack_id for layer in self.layers.values()]): + 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]] + + kwargs.setdefault("hostname", stack.generate_hostname()) + kwargs.setdefault("ssh_keyname", stack.default_ssh_keyname) + kwargs.setdefault("availability_zone", stack.default_availability_zone) + kwargs.setdefault("subnet_id", stack.default_subnet_id) + kwargs.setdefault("root_device_type", stack.default_root_device_type) + if layer.custom_instance_profile_arn: + kwargs.setdefault("instance_profile_arn", layer.custom_instance_profile_arn) + kwargs.setdefault("instance_profile_arn", stack.default_instance_profile_arn) + kwargs.setdefault("security_group_ids", layer.custom_security_group_ids) + kwargs.setdefault("associate_public_ip", layer.auto_assign_public_ips) + kwargs.setdefault("ebs_optimized", layer.use_ebs_optimized_instances) + kwargs.update({"ec2_backend": self.ec2_backend}) + opsworks_instance = OpsworkInstance(**kwargs) + self.instances[opsworks_instance.id] = opsworks_instance + return opsworks_instance + def describe_stacks(self, stack_ids): if stack_ids is None: return [stack.to_dict() for stack in self.stacks.values()] @@ -297,7 +483,6 @@ class OpsWorksBackend(BaseBackend): return [self.layers[id].to_dict() for id in layer_ids] - opsworks_backends = {} for region, ec2_backend in ec2_backends.items(): - opsworks_backends[region] = OpsWorksBackend(ec2_backend, elb_backends[region]) + opsworks_backends[region] = OpsWorksBackend(ec2_backend) diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py index 01600b91..cd6bfce8 100644 --- a/moto/opsworks/responses.py +++ b/moto/opsworks/responses.py @@ -64,6 +64,29 @@ class OpsWorksResponse(BaseResponse): layer = self.opsworks_backend.create_layer(**kwargs) return json.dumps({"LayerId": layer.id}, indent=1) + def create_instance(self): + kwargs = dict( + stack_id=self.parameters.get("StackId"), + layer_ids=self.parameters.get("LayerIds"), + instance_type=self.parameters.get("InstanceType"), + auto_scale_type=self.parameters.get("AutoScalingType"), + hostname=self.parameters.get("Hostname"), + os=self.parameters.get("Os"), + ami_id=self.parameters.get("AmiId"), + ssh_keyname=self.parameters.get("SshKeyName"), + availability_zone=self.parameters.get("AvailabilityZone"), + virtualization_type=self.parameters.get("VirtualizationType"), + subnet_id=self.parameters.get("SubnetId"), + architecture=self.parameters.get("Architecture"), + root_device_type=self.parameters.get("RootDeviceType"), + block_device_mappings=self.parameters.get("BlockDeviceMappings"), + install_updates_on_boot=self.parameters.get("InstallUpdatesOnBoot"), + ebs_optimized=self.parameters.get("EbsOptimized"), + agent_version=self.parameters.get("AgentVersion"), + ) + opsworks_instance = self.opsworks_backend.create_instance(**kwargs) + return json.dumps({"InstanceId": opsworks_instance.id}, indent=1) + def describe_stacks(self): stack_ids = self.parameters.get("StackIds") stacks = self.opsworks_backend.describe_stacks(stack_ids) @@ -73,4 +96,5 @@ 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}, indent=1) + return json.dumps({"Layers": layers}) + diff --git a/tests/test_opsworks/test_instances.py b/tests/test_opsworks/test_instances.py new file mode 100644 index 00000000..1050093d --- /dev/null +++ b/tests/test_opsworks/test_instances.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa +import re + +from moto import mock_opsworks + +@mock_opsworks +def test_create_instance_response(): + client = boto3.client('opsworks') + stack_id = client.create_stack( + Name="test_stack_1", + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + )['StackId'] + + layer_id = client.create_layer( + StackId=stack_id, + Type="custom", + Name="TestLayer", + Shortname="TestLayerShortName" + )['LayerId'] + + response = client.create_instance( + StackId=stack_id, LayerIds=[layer_id], InstanceType="t2.micro" + ) + + response.should.contain("InstanceId") + From 1ce22068ea573d5e749342d7076455cef3ac82bc Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Mon, 18 Apr 2016 15:44:21 -0400 Subject: [PATCH 4/7] opsworks: impl start_instance, describe_instances --- moto/core/responses.py | 1 - moto/opsworks/models.py | 51 ++++++++- moto/opsworks/responses.py | 14 ++- tests/test_opsworks/test_instances.py | 150 +++++++++++++++++++++++++- tests/test_opsworks/test_stack.py | 2 +- 5 files changed, 208 insertions(+), 10 deletions(-) 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", From 9ce1890f358fbf245ac07efbcbad26bae274bf74 Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Mon, 18 Apr 2016 16:03:13 -0400 Subject: [PATCH 5/7] opsworks/tests: init boto3.client with region_name='us-east-1' --- tests/test_opsworks/test_instances.py | 6 +++--- tests/test_opsworks/test_layers.py | 4 ++-- tests/test_opsworks/test_stack.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_opsworks/test_instances.py b/tests/test_opsworks/test_instances.py index 0175eec4..28187c2b 100644 --- a/tests/test_opsworks/test_instances.py +++ b/tests/test_opsworks/test_instances.py @@ -8,7 +8,7 @@ from moto import mock_ec2 @mock_opsworks def test_create_instance(): - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') stack_id = client.create_stack( Name="test_stack_1", Region="us-east-1", @@ -48,7 +48,7 @@ def test_describe_instances(): populate S2L2 with 3 instances (S2L2_i1..2) """ - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') S1 = client.create_stack( Name="S1", Region="us-east-1", @@ -138,7 +138,7 @@ def test_ec2_integration(): instances created via OpsWorks should be discoverable via ec2 """ - opsworks = boto3.client('opsworks') + opsworks = boto3.client('opsworks', region_name='us-east-1') stack_id = opsworks.create_stack( Name="S1", Region="us-east-1", diff --git a/tests/test_opsworks/test_layers.py b/tests/test_opsworks/test_layers.py index 128a846f..005fa192 100644 --- a/tests/test_opsworks/test_layers.py +++ b/tests/test_opsworks/test_layers.py @@ -8,7 +8,7 @@ from moto import mock_opsworks @mock_opsworks def test_create_layer_response(): - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') stack_id = client.create_stack( Name="test_stack_1", Region="us-east-1", @@ -47,7 +47,7 @@ def test_create_layer_response(): @mock_opsworks def test_describe_layers(): - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') stack_id = client.create_stack( Name="test_stack_1", Region="us-east-1", diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py index 8bb682ca..874c09ff 100644 --- a/tests/test_opsworks/test_stack.py +++ b/tests/test_opsworks/test_stack.py @@ -8,7 +8,7 @@ from moto import mock_opsworks @mock_opsworks def test_create_stack_response(): - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') response = client.create_stack( Name="test_stack_1", Region="us-east-1", @@ -20,7 +20,7 @@ def test_create_stack_response(): @mock_opsworks def test_describe_stacks(): - client = boto3.client('opsworks') + client = boto3.client('opsworks', region_name='us-east-1') for i in range(1, 4): client.create_stack( Name="test_stack_{}".format(i), From 3a8268fd5609e454850477d7e8d7876c7f66cb65 Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Mon, 18 Apr 2016 16:08:08 -0400 Subject: [PATCH 6/7] opsworks/tests: add missing region_name='us-east-1' in client init --- tests/test_opsworks/test_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_opsworks/test_instances.py b/tests/test_opsworks/test_instances.py index 28187c2b..e24486a2 100644 --- a/tests/test_opsworks/test_instances.py +++ b/tests/test_opsworks/test_instances.py @@ -157,7 +157,7 @@ def test_ec2_integration(): StackId=stack_id, LayerIds=[layer_id], InstanceType="t2.micro" )['InstanceId'] - ec2 = boto3.client('ec2') + ec2 = boto3.client('ec2', region_name='us-east-1') # Before starting the instance, it shouldn't be discoverable via ec2 reservations = ec2.describe_instances()['Reservations'] From 768f1adb4fd7789e0a92863283457a096e15962e Mon Sep 17 00:00:00 2001 From: Vladimir Sudilovsky Date: Mon, 18 Apr 2016 16:36:30 -0400 Subject: [PATCH 7/7] opsworks: py2.6 compat str.format --- moto/opsworks/models.py | 20 ++++++++++---------- tests/test_opsworks/test_stack.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index 8a3cdd16..68edade9 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -77,7 +77,7 @@ class OpsworkInstance(object): self.infrastructure_class = "ec2 (fixed)" self.platform = "linux (fixed)" - self.id = "{}".format(uuid.uuid4()) + self.id = "{0}".format(uuid.uuid4()) self.created_at = datetime.datetime.utcnow() def start(self): @@ -249,7 +249,7 @@ class Layer(object): self.install_updates_on_boot = install_updates_on_boot self.use_ebs_optimized_instances = use_ebs_optimized_instances - self.id = "{}".format(uuid.uuid4()) + self.id = "{0}".format(uuid.uuid4()) self.created_at = datetime.datetime.utcnow() def __eq__(self, other): @@ -338,7 +338,7 @@ class Stack(object): self.default_root_device_type = default_root_device_type self.agent_version = agent_version - self.id = "{}".format(uuid.uuid4()) + self.id = "{0}".format(uuid.uuid4()) self.layers = [] self.apps = [] self.account_number = "123456789012" @@ -417,11 +417,11 @@ class OpsWorksBackend(BaseBackend): raise ResourceNotFoundException(stackid) if name in [l.name for l in self.layers.values()]: raise ValidationException( - 'There is already a layer named "{}" ' + 'There is already a layer named "{0}" ' 'for this stack'.format(name)) if shortname in [l.shortname for l in self.layers.values()]: raise ValidationException( - 'There is already a layer with shortname "{}" ' + 'There is already a layer with shortname "{0}" ' 'for this stack'.format(shortname)) layer = Layer(**kwargs) self.layers[layer.id] = layer @@ -434,7 +434,7 @@ class OpsWorksBackend(BaseBackend): if stack_id not in self.stacks: raise ResourceNotFoundException( - "Unable to find stack with ID {}".format(stack_id)) + "Unable to find stack with ID {0}".format(stack_id)) unknown_layers = set(layer_ids) - set(self.layers.keys()) if unknown_layers: @@ -484,7 +484,7 @@ class OpsWorksBackend(BaseBackend): if stack_id is not None: if stack_id not in self.stacks: raise ResourceNotFoundException( - "Unable to find stack with ID {}".format(stack_id)) + "Unable to find stack with ID {0}".format(stack_id)) return [layer.to_dict() for layer in self.stacks[stack_id].layers] unknown_layers = set(layer_ids) - set(self.layers.keys()) @@ -506,21 +506,21 @@ class OpsWorksBackend(BaseBackend): if layer_id: if layer_id not in self.layers: raise ResourceNotFoundException( - "Unable to find layer with ID {}".format(layer_id)) + "Unable to find layer with ID {0}".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)) + "Unable to find stack with ID {0}".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)) + "Unable to find instance with ID {0}".format(instance_id)) self.instances[instance_id].start() diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py index 874c09ff..8d86e420 100644 --- a/tests/test_opsworks/test_stack.py +++ b/tests/test_opsworks/test_stack.py @@ -23,7 +23,7 @@ def test_describe_stacks(): client = boto3.client('opsworks', region_name='us-east-1') for i in range(1, 4): client.create_stack( - Name="test_stack_{}".format(i), + Name="test_stack_{0}".format(i), Region="us-east-1", ServiceRoleArn="service_arn", DefaultInstanceProfileArn="profile_arn"