Repository: aws/aws-cli

Issue:
ec2 run-instances ignores count in its json input
When preparing a reservation with `aws ec2 run-instances --cli-input-json JSONBLOB` the `MinCount` and `MaxCount` parameters are ignored and overwritten by the value 1. The equivalent reservation using command line options and esp. the `--count` option functions as expected.

How to reproduce:

``` shell
jsonblob()
{
   cat <<'EOF'
{
  "DryRun": false,
  "ImageId": "ami-XXXXXXXX",
  "MinCount": 3,
  "MaxCount": 3,
  "SubnetId": "subnet-XXXXXXXX",
  "UserData": "IyEvYmluL3NoCnNodXRkb3duIC1QICsyCg==",
  "InstanceInitiatedShutdownBehavior": "terminate",
  "InstanceType": "t2.small"
}
EOF
}
aws --debug ec2 run-instances --cli-input-json "$(jsonblob)"
```

(The user data is a script terminating the instance after a few minutes.)

The output contains the line

```
2015-09-23 16:49:09,762 - MainThread - botocore.endpoint - DEBUG - Making request for <botocore.model.OperationModel object at 0xXXXXXXXXXX> (verify_ssl=True) with params: {'body': {u'UserData': u'SXlFdlltbHVMM05vQ25Ob2RYUmtiM2R1SUMxUUlDc3lDZz09', u'DryRun': 'false', u'InstanceInitiatedShutdownBehavior': u'terminate', u'ImageId': u'ami-XXXXXXXX', u'SubnetId': u'subnet-XXXXXXXX', 'Version': u'2015-04-15', 'MaxCount': 1, 'Action': u'RunInstances', 'MinCount': 1, u'InstanceType': u't2.small'}, 'url': u'https://ec2.us-east-1.amazonaws.com/', 'headers': {'User-Agent': 'aws-cli/1.7.32 Python/2.7.10 Darwin/14.5.0'}, 'query_string': '', 'url_path': '/', 'method': u'POST'}
```

This demonstrates that the parameters `MaxCount` and `MinCount` in the JSON input are ignored.

---

``` console
% pip show awscli

---
Metadata-Version: 1.1
Name: awscli
Version: 1.7.32
Summary: Universal Command Line Environment for AWS.
Home-page: http://aws.amazon.com/cli/
Author: Mitch Garnaat
Author-email: garnaat@amazon.com
License: Apache License 2.0
Location: /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages
Requires: botocore, bcdoc, colorama, docutils, rsa
```



Discussion:
The bug is still in the latest version available throug PIP

```
%  pip show awscli        
---
Metadata-Version: 2.0
Name: awscli
Version: 1.8.6
Summary: Universal Command Line Environment for AWS.
Home-page: http://aws.amazon.com/cli/
Author: Amazon Web Services
Author-email: UNKNOWN
License: Apache License 2.0
Location: /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages
Requires: colorama, botocore, rsa, docutils
```

And quite interestingly the `--count` option can be used together with the JSON blob to set these parameters, just append the `--count 3` option to the command line in the example above to demonstrate it.

Yeah that is unfortunate. The workaround that you mentioned is what I would have suggested. The reason why it happens is the `--count` parameter is a custom parameter that has a default of 1 if it is missing from the command line. However, with the `--cli-input-json` parameter we allow users to override values in there by specifying it on the command line. However, what is happening here is that even though the parameter is missing, a default value is provided and thus clobbers the value in the JSON skeleton provided. We will look to see how we can fix this.


Resolving patch:
diff --git a/awscli/customizations/ec2addcount.py b/awscli/customizations/ec2addcount.py
--- a/awscli/customizations/ec2addcount.py
+++ b/awscli/customizations/ec2addcount.py
@@ -20,12 +20,20 @@
 logger = logging.getLogger(__name__)
 
 
+DEFAULT = 1
 HELP = """
 <p>Number of instances to launch. If a single number is provided, it
-is assumed to be the minimum to launch (defaults to 1).  If a range is
+is assumed to be the minimum to launch (defaults to %d).  If a range is
 provided in the form <code>min:max</code> then the first number is
 interpreted as the minimum number of instances to launch and the second
-is interpreted as the maximum number of instances to launch.</p>"""
+is interpreted as the maximum number of instances to launch.</p>""" % DEFAULT
+
+
+def register_count_events(event_handler):
+    event_handler.register(
+        'building-argument-table.ec2.run-instances', ec2_add_count)
+    event_handler.register(
+        'before-parameter-build.ec2.RunInstances', set_default_count)
 
 
 def ec2_add_count(argument_table, **kwargs):
@@ -34,6 +42,11 @@ def ec2_add_count(argument_table, **kwargs):
     del argument_table['max-count']
 
 
+def set_default_count(params, **kwargs):
+    params.setdefault('MaxCount', DEFAULT)
+    params.setdefault('MinCount', DEFAULT)
+
+
 class CountArgument(BaseCLIArgument):
 
     def __init__(self, name):
@@ -62,11 +75,14 @@ def documentation(self):
         return HELP
 
     def add_to_parser(self, parser):
+        # We do NOT set default value here. It will be set later by event hook.
         parser.add_argument(self.cli_name, metavar=self.py_name,
-                            help='Number of instances to launch',
-                            default='1')
+                            help='Number of instances to launch')
 
     def add_to_params(self, parameters, value):
+        if value is None:
+            # NO-OP if value is not explicitly set by user
+            return
         try:
             if ':' in value:
                 minstr, maxstr = value.split(':')
diff --git a/awscli/handlers.py b/awscli/handlers.py
--- a/awscli/handlers.py
+++ b/awscli/handlers.py
@@ -22,7 +22,7 @@
 from awscli.customizations.streamingoutputarg import add_streaming_output_arg
 from awscli.customizations.addexamples import add_examples
 from awscli.customizations.removals import register_removals
-from awscli.customizations.ec2addcount import ec2_add_count
+from awscli.customizations.ec2addcount import register_count_events
 from awscli.customizations.paginate import register_pagination
 from awscli.customizations.ec2decryptpassword import ec2_add_priv_launch_key
 from awscli.customizations.ec2secgroupsimplify import register_secgroup
@@ -92,8 +92,7 @@ def awscli_initialize(event_handlers):
     register_cli_input_json(event_handlers)
     event_handlers.register('building-argument-table.*',
                             add_streaming_output_arg)
-    event_handlers.register('building-argument-table.ec2.run-instances',
-                            ec2_add_count)
+    register_count_events(event_handlers)
     event_handlers.register('building-argument-table.ec2.get-password-data',
                             ec2_add_priv_launch_key)
     register_parse_global_args(event_handlers)


Test patch:
diff --git a/tests/functional/ec2/test_run_instances.py b/tests/functional/ec2/test_run_instances.py
--- a/tests/functional/ec2/test_run_instances.py
+++ b/tests/functional/ec2/test_run_instances.py
@@ -68,6 +68,19 @@ def test_count_range(self):
         }
         self.assert_params_for_cmd(args_list, result)
 
+    def test_count_in_json_only(self):
+        input_json = '{"ImageId":"ami-xxxx","MaxCount":9,"MinCount":5}'
+        args_list = (self.prefix + ' --cli-input-json ' + input_json).split()
+        result = {'ImageId': 'ami-xxxx', 'MaxCount': 9, 'MinCount': 5}
+        self.assert_params_for_cmd(args_list, result)
+
+    def test_count_in_cli_and_in_json(self):
+        input_json = '{"ImageId":"ami-xxxx","MaxCount":9,"MinCount":5}'
+        args_list = (
+            self.prefix + ' --count 3 --cli-input-json ' + input_json).split()
+        result = {'ImageId': 'ami-xxxx', 'MaxCount': 3, 'MinCount': 3}
+        self.assert_params_for_cmd(args_list, result)
+
     def test_block_device_mapping(self):
         args = ' --image-id ami-foobar --count 1'
         args_list = (self.prefix + args).split()