diff --git a/moto/__init__.py b/moto/__init__.py index d497eec02..ec39560b7 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 b66af5778..d1262a7cb 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 79b0af637..5f40abb3e 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 000000000..dfcd582e2 --- /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 000000000..e41cc7a35 --- /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 000000000..34114b4f3 --- /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 000000000..9b5d7f15d --- /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 000000000..6913de6bb --- /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 000000000..1555fa18f --- /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) + +