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/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..b408b82f3 --- /dev/null +++ b/moto/opsworks/exceptions.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class ResourceNotFoundException(BadRequest): + def __init__(self, message): + 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 new file mode 100644 index 000000000..68edade9a --- /dev/null +++ b/moto/opsworks/models.py @@ -0,0 +1,529 @@ +from __future__ import unicode_literals +from moto.core import BaseBackend +from moto.ec2 import ec2_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 + # 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 = [{ + '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 = "{0}".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.instance 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.root_device_type = self.instance.root_device_type + + 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, + "InstanceProfileArn": self.instance_profile_arn, + "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: + 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 + + +class Layer(object): + 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.enable_autohealing = enable_autohealing + self.auto_assign_elastic_ips = auto_assign_elastic_ips + self.auto_assign_public_ips = auto_assign_public_ips + self.install_updates_on_boot = install_updates_on_boot + self.use_ebs_optimized_instances = use_ebs_optimized_instances + + self.id = "{0}".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): + def __init__(self, name, region, service_role_arn, default_instance_profile_arn, + vpcid="vpc-1f99bf7a", + 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 = "{0}".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 + + 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 range(4)]) + + @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): + self.stacks = {} + self.layers = {} + self.instances = {} + self.ec2_backend = ec2_backend + + def reset(self): + ec2_backend = self.ec2_backend + self.__dict__ = {} + self.__init__(ec2_backend) + + def create_stack(self, **kwargs): + stack = Stack(**kwargs) + self.stacks[stack.id] = stack + return stack + + 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 "{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 "{0}" ' + 'for this stack'.format(shortname)) + layer = Layer(**kwargs) + self.layers[layer.id] = layer + 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 {0}".format(stack_id)) + + unknown_layers = set(layer_ids) - set(self.layers.keys()) + if unknown_layers: + raise ResourceNotFoundException(", ".join(unknown_layers)) + + 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 = layers[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()] + + unknown_stacks = set(stack_ids) - set(self.stacks.keys()) + if 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 {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()) + if unknown_layers: + 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 {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 {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 {0}".format(instance_id)) + self.instances[instance_id].start() + + +opsworks_backends = {} +for region, ec2_backend in ec2_backends.items(): + opsworks_backends[region] = OpsWorksBackend(ec2_backend) diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py new file mode 100644 index 000000000..47fed3016 --- /dev/null +++ b/moto/opsworks/responses.py @@ -0,0 +1,112 @@ +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 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 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) + 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) + + 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/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_instances.py b/tests/test_opsworks/test_instances.py new file mode 100644 index 000000000..e24486a2f --- /dev/null +++ b/tests/test_opsworks/test_instances.py @@ -0,0 +1,176 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa + +from moto import mock_opsworks +from moto import mock_ec2 + + +@mock_opsworks +def test_create_instance(): + client = boto3.client('opsworks', region_name='us-east-1') + 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") + + 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', region_name='us-east-1') + 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', region_name='us-east-1') + 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', region_name='us-east-1') + + # 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_layers.py b/tests/test_opsworks/test_layers.py new file mode 100644 index 000000000..005fa192e --- /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', region_name='us-east-1') + 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', region_name='us-east-1') + 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 new file mode 100644 index 000000000..8d86e4207 --- /dev/null +++ b/tests/test_opsworks/test_stack.py @@ -0,0 +1,48 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa +import re + +from moto import mock_opsworks + + +@mock_opsworks +def test_create_stack_response(): + client = boto3.client('opsworks', region_name='us-east-1') + 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', region_name='us-east-1') + for i in range(1, 4): + client.create_stack( + Name="test_stack_{0}".format(i), + Region="us-east-1", + ServiceRoleArn="service_arn", + DefaultInstanceProfileArn="profile_arn" + ) + + response = client.describe_stacks() + response['Stacks'].should.have.length_of(3) + 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') + ) + +