diff --git a/moto/opsworks/exceptions.py b/moto/opsworks/exceptions.py index e41cc7a35..b408b82f3 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 34114b4f3..af506bd24 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 9b5d7f15d..01600b91c 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 000000000..128a846f8 --- /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 1555fa18f..8e17c9410 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') + ) +