Repository: getmoto/moto

Issue:
EC2 modify_attribute blockDeviceMapping doesn't work
Code to reproduce:

``` python
import unittest

from boto.ec2 import EC2Connection
from moto import mock_ec2


class PoC(unittest.TestCase):

    @mock_ec2
    def test_poc(self):
        conn = EC2Connection()

        reservation = conn.run_instances('ami-48eb2f20',
                                         placement='us-east-1d',
                                         key_name='keypair',
                                         instance_type='m1.medium')

        instance = reservation.instances[0]
        instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True})
```

Running the code:

``` console
$ nosetests poc.py
E
======================================================================
ERROR: test_poc (poc.PoC)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/moto/core/models.py", line 61, in wrapper
    result = func(*args, **kwargs)
  File "/home/pablo/pch/bouncer/poc.py", line 19, in test_poc
    instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True})
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/ec2/instance.py", line 557, in modify_attribute
    dry_run=dry_run
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/ec2/connection.py", line 1233, in modify_instance_attribute
    return self.get_status('ModifyInstanceAttribute', params, verb='POST')
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/connection.py", line 1152, in get_status
    response = self.make_request(action, params, path, verb)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/connection.py", line 1059, in make_request
    return self._mexe(http_request)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/connection.py", line 898, in _mexe
    response = connection.getresponse()
  File "/usr/lib/python2.7/httplib.py", line 1043, in getresponse
    response = self.response_class(*args, **kwds)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/boto/connection.py", line 392, in __init__
    httplib.HTTPResponse.__init__(self, *args, **kwargs)
  File "/usr/lib/python2.7/httplib.py", line 346, in __init__
    self.fp = sock.makefile('rb', 0)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/httpretty/core.py", line 314, in makefile
    self._entry.fill_filekind(self.fd)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/httpretty/core.py", line 564, in fill_filekind
    status, headers, self.body = self.callable_body(self.request, self.info.full_url(), headers)
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/moto/core/responses.py", line 43, in dispatch
    return self.call_action()
  File "/home/pablo/pch/virtual-envs/bouncer/local/lib/python2.7/site-packages/moto/core/responses.py", line 56, in call_action
    body, new_headers = response
TypeError: 'NoneType' object is not iterable

----------------------------------------------------------------------
Ran 1 test in 0.023s

FAILED (errors=1)
$ 
```

Commenting the last line in the python file makes the test PASS.



Discussion:
Also fails with latest moto from pypi: moto==0.3.3

Code fails because we get here https://github.com/spulec/moto/blob/6c3fb51d105d9fc130cd17b4a7d570b132f85d8b/moto/ec2/responses/instances.py#L86

List of key and values are:
- SignatureVersion : ['2']
- AWSAccessKeyId : ['...']
- InstanceId : ['i-d7d82136']
- Timestamp : ['2014-08-13T16:15:56Z']
- BlockDeviceMapping.1.Ebs.DeleteOnTermination : ['true']
- BlockDeviceMapping.1.DeviceName : ['/dev/sda1']
- SignatureMethod : ['HmacSHA256']
- Version : ['2013-10-15']
- Signature : ['...]
- Action : ['ModifyInstanceAttribute']

`if '.Value' in key:` never matches, this indicates that there is no support for this EC2 feature.

http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html

It seems that a solution would be to have a model for BlockDeviceMapping:

```
from boto.ec2.blockdevicemapping import BlockDeviceMapping
```

Associated with each instance.

If you guide me on the best way to solve this, I could send a PR, but don't want to write code that does work but is not aligned with your architecture.

Yes, that all sounds correct.

We do implement this for LaunchConfigurations [here](https://github.com/spulec/moto/blame/master/moto/autoscaling/models.py#L86-101) with [tests](https://github.com/spulec/moto/blob/ba012d990277afdb5e39f0bfedf0fcad36879c7f/tests/test_autoscaling/test_launch_configurations.py#L41-L99) so I imagine we would want to do similar.


Resolving patch:
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -3,6 +3,7 @@
 from collections import defaultdict
 
 from boto.ec2.instance import Instance as BotoInstance, Reservation
+from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
 from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest
 from boto.ec2.launchspecification import LaunchSpecification
 
@@ -62,6 +63,9 @@ def __init__(self, image_id, user_data, security_groups, **kwargs):
         self.subnet_id = kwargs.get("subnet_id")
         self.key_name = kwargs.get("key_name")
 
+        self.block_device_mapping = BlockDeviceMapping()
+        self.block_device_mapping['/dev/sda1'] = BlockDeviceType()
+
     @classmethod
     def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
         properties = cloudformation_json['Properties']
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -66,7 +66,8 @@ def start_instances(self):
         return template.render(instances=instances)
 
     def describe_instance_attribute(self):
-        # TODO this and modify below should raise IncorrectInstanceState if instance not in stopped state
+        # TODO this and modify below should raise IncorrectInstanceState if
+        # instance not in stopped state
         attribute = self.querystring.get("Attribute")[0]
         key = camelcase_to_underscores(attribute)
         instance_ids = instance_ids_from_querystring(self.querystring)
@@ -76,6 +77,61 @@ def describe_instance_attribute(self):
         return template.render(instance=instance, attribute=attribute, value=value)
 
     def modify_instance_attribute(self):
+        handlers = [self._dot_value_instance_attribute_handler,
+                    self._block_device_mapping_handler]
+
+        for handler in handlers:
+            success = handler()
+            if success:
+                return success
+
+        msg = "This specific call to ModifyInstanceAttribute has not been" \
+              " implemented in Moto yet. Feel free to open an issue at" \
+              " https://github.com/spulec/moto/issues"
+        raise NotImplementedError(msg)
+
+    def _block_device_mapping_handler(self):
+        """
+        Handles requests which are generated by code similar to:
+
+            instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True})
+
+        The querystring contains information similar to:
+
+            BlockDeviceMapping.1.Ebs.DeleteOnTermination : ['true']
+            BlockDeviceMapping.1.DeviceName : ['/dev/sda1']
+
+        For now we only support the "BlockDeviceMapping.1.Ebs.DeleteOnTermination"
+        configuration, but it should be trivial to add anything else.
+        """
+        mapping_counter = 1
+        mapping_device_name_fmt = 'BlockDeviceMapping.%s.DeviceName'
+        mapping_del_on_term_fmt = 'BlockDeviceMapping.%s.Ebs.DeleteOnTermination'
+
+        while True:
+            mapping_device_name = mapping_device_name_fmt % mapping_counter
+            if mapping_device_name not in self.querystring.keys():
+                break
+
+            mapping_del_on_term = mapping_del_on_term_fmt % mapping_counter
+            del_on_term_value_str = self.querystring[mapping_del_on_term][0]
+            del_on_term_value = True if 'true' == del_on_term_value_str else False
+            device_name_value = self.querystring[mapping_device_name][0]
+
+            instance_ids = instance_ids_from_querystring(self.querystring)
+            instance_id = instance_ids[0]
+            instance = ec2_backend.get_instance(instance_id)
+
+            block_device_type = instance.block_device_mapping[device_name_value]
+            block_device_type.delete_on_termination = del_on_term_value
+
+            # +1 for the next device
+            mapping_counter += 1
+
+        if mapping_counter > 1:
+            return EC2_MODIFY_INSTANCE_ATTRIBUTE
+
+    def _dot_value_instance_attribute_handler(self):
         attribute_key = None
         for key, value in self.querystring.iteritems():
             if '.Value' in key:
@@ -84,6 +140,7 @@ def modify_instance_attribute(self):
 
         if not attribute_key:
             return
+
         value = self.querystring.get(attribute_key)[0]
         normalized_attribute = camelcase_to_underscores(attribute_key.split(".")[0])
         instance_ids = instance_ids_from_querystring(self.querystring)


Test patch:
diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py
--- a/tests/test_ec2/test_elastic_block_store.py
+++ b/tests/test_ec2/test_elastic_block_store.py
@@ -3,6 +3,7 @@
 import sure  # noqa
 
 from moto import mock_ec2
+from moto.ec2.models import ec2_backend
 
 
 @mock_ec2
@@ -76,3 +77,24 @@ def test_create_snapshot():
 
     # Deleting something that was already deleted should throw an error
     snapshot.delete.when.called_with().should.throw(EC2ResponseError)
+
+
+@mock_ec2
+def test_modify_attribute_blockDeviceMapping():
+    """
+    Reproduces the missing feature explained at [0], where we want to mock a
+    call to modify an instance attribute of type: blockDeviceMapping.
+
+    [0] https://github.com/spulec/moto/issues/160
+    """
+    conn = boto.connect_ec2('the_key', 'the_secret')
+
+    reservation = conn.run_instances('ami-1234abcd')
+
+    instance = reservation.instances[0]
+
+    instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True})
+
+    instance = ec2_backend.get_instance(instance.id)
+    instance.block_device_mapping.should.have.key('/dev/sda1')
+    instance.block_device_mapping['/dev/sda1'].delete_on_termination.should.be(True)