Repository: aws/aws-cli

Issue:
--associate-public-ip-address option does not work
when runnning aws ec2 run-instances as follows

> aws ec2 run-instances --output text \
> --image-id ami-xxxxxxxx \
> --key-name ap-southeast-2 \
> --security-group-ids sg-zzzzzzzz \
> --instance-type t1.micro \
> --subnet-id subnet-yyyyyyyy \
> --disable-api-termination \
> --no-ebs-optimized \
> --associate-public-ip-address  \
> --count 1 \
> --region ap-southeast-2 --debug

the error message is shown like below.

> Network interfaces and an instance-level subnet ID may not be specified on the same request

I compared the https API request between ec2-api-tools and awscli and in this case there are some differences between them as follow. It seems that the paramers such as subnet-id and security-group-ids is not handled correctly.

ec2-api-tools

> NetworkInterface.1.AssociatePublicIpAddress=true&
> NetworkInterface.1.DeviceIndex=0&
> NetworkInterface.1.SecurityGroupId.1=sg-zzzzzzzz&
> NetworkInterface.1.SubnetId=subnet-yyyyyyyy&

awscli

> NetworkInterface.1.AssociatePublicIpAddress=true&
> NetworkInterface.1.DeviceIndex=0&
> SecurityGroupId.1=sg-zzzzzzzz&
> SubnetId=subnet-yyyyyyyy&



Discussion:

Resolving patch:
diff --git a/awscli/customizations/ec2runinstances.py b/awscli/customizations/ec2runinstances.py
--- a/awscli/customizations/ec2runinstances.py
+++ b/awscli/customizations/ec2runinstances.py
@@ -24,7 +24,6 @@
 """
 from awscli.arguments import CustomArgument
 
-
 # --secondary-private-ip-address
 SECONDARY_PRIVATE_IP_ADDRESSES_DOCS = (
     '[EC2-VPC] A secondary private IP address for the network interface '
@@ -43,8 +42,6 @@
     '[EC2-VPC] If specified a public IP address will be assigned '
     'to the new instance in a VPC.')
 
-
-
 def _add_params(argument_table, operation, **kwargs):
     arg = SecondaryPrivateIpAddressesArgument(
         name='secondary-private-ip-addresses',
@@ -81,9 +78,23 @@ def _check_args(parsed_args, **kwargs):
                        'not supported.')
                 raise ValueError(msg)
 
+
+def _fix_subnet(operation, endpoint, params, **kwargs):
+    # If the user has supplied a --subnet-id option AND we also
+    # have inserted an AssociatePublicIpAddress into the network_interfaces
+    # structure, we need to move the subnetId value down into the
+    # network_interfaces structure or we will get a client error from EC2.
+    if 'network_interfaces' in params:
+        ni = params['network_interfaces']
+        if 'AssociatePublicIpAddress' in ni[0]:
+            if 'subnet_id' in params:
+                ni[0]['SubnetId'] = params['subnet_id']
+                del params['subnet_id']
+
 EVENTS = [
     ('building-argument-table.ec2.run-instances', _add_params),
     ('operation-args-parsed.ec2.run-instances', _check_args),
+    ('before-parameter-build.ec2.RunInstances', _fix_subnet),
     ]
 
 


Test patch:
diff --git a/tests/unit/ec2/test_run_instances.py b/tests/unit/ec2/test_run_instances.py
--- a/tests/unit/ec2/test_run_instances.py
+++ b/tests/unit/ec2/test_run_instances.py
@@ -111,28 +111,55 @@ def test_secondary_ip_address_count(self):
         self.assert_params_for_cmd(args_list, result)
 
     def test_associate_public_ip_address(self):
-        args = ' --image-id ami-foobar --count 1 '
+        args = ' --image-id ami-foobar --count 1 --subnet-id subnet-12345678 '
         args += '--associate-public-ip-address'
         args_list = (self.prefix + args).split()
         result = {
             'NetworkInterface.1.DeviceIndex': '0',
             'NetworkInterface.1.AssociatePublicIpAddress': 'true',
+            'NetworkInterface.1.SubnetId': 'subnet-12345678',
             'ImageId': 'ami-foobar',
             'MaxCount': '1',
             'MinCount': '1'
         }
         self.assert_params_for_cmd(args_list, result)
 
-    def test_no_associate_public_ip_address(self):
+    def test_associate_public_ip_address_switch_order(self):
         args = ' --image-id ami-foobar --count 1 '
+        args += '--associate-public-ip-address --subnet-id subnet-12345678'
+        args_list = (self.prefix + args).split()
+        result = {
+            'NetworkInterface.1.DeviceIndex': '0',
+            'NetworkInterface.1.AssociatePublicIpAddress': 'true',
+            'NetworkInterface.1.SubnetId': 'subnet-12345678',
+            'ImageId': 'ami-foobar',
+            'MaxCount': '1',
+            'MinCount': '1'
+        }
+        self.assert_params_for_cmd(args_list, result)
+
+    def test_no_associate_public_ip_address(self):
+        args = ' --image-id ami-foobar --count 1  --subnet-id subnet-12345678 '
         args += '--no-associate-public-ip-address'
         args_list = (self.prefix + args).split()
         result = {
             'NetworkInterface.1.DeviceIndex': '0',
             'NetworkInterface.1.AssociatePublicIpAddress': 'false',
+            'NetworkInterface.1.SubnetId': 'subnet-12345678',
             'ImageId': 'ami-foobar',
             'MaxCount': '1',
             'MinCount': '1'
         }
         self.assert_params_for_cmd(args_list, result)
         
+    def test_subnet_alone(self):
+        args = ' --image-id ami-foobar --count 1 --subnet-id subnet-12345678'
+        args_list = (self.prefix + args).split()
+        result = {
+            'SubnetId': 'subnet-12345678',
+            'ImageId': 'ami-foobar',
+            'MaxCount': '1',
+            'MinCount': '1'
+        }
+        self.assert_params_for_cmd(args_list, result)
+