Repository: ansible/ansible-modules-core

Issue:
ec2_asg with tags completely broken
##### Issue Type:

Bug Report
##### Ansible Version:

```
$ ansible --version
ansible 1.8 (devel 547c0d8140) last updated 2014/10/27 18:11:04 (GMT +200)
  lib/ansible/modules/core: (detached HEAD 7f611468a8) last updated 2014/10/27 18:11:10 (GMT +200)
  lib/ansible/modules/extras: (detached HEAD a0df36c6ab) last updated 2014/10/27 18:11:15 (GMT +200)
  v2/ansible/modules/core: (detached HEAD cb69744bce) last updated 2014/10/27 18:11:21 (GMT +200)
  v2/ansible/modules/extras: (detached HEAD 8a4f07eecd) last updated 2014/10/27 18:11:26 (GMT +200)
  configured module search path = None
```
##### Environment:

Linux
##### Summary:

ec2_asg fails spectacularly when it is used with a _tags_ argument.
##### Steps To Reproduce:

Any ec2_asg task that has a _tags_ argument will trigger this bug, e.g.:

``` yaml
- ec2_asg:
    name: foo
    launch_config_name: 'lc-1'
    min_size: 1
    max_size: 1
    desired_capacity: 1
    vpc_zone_identifier: [ 'subnet-abcd1234' ]
    tags:
      - foo: bar
```
##### Expected Results:

The ASG should be created if missing. Otherwise the task should be skipped.
##### Actual Results:

Ansible will fail in one of two ways, depending on whether
any tagged auto scaling groups exist on AWS already.
##### Case 1: No tagged ASG  exists on AWS yet

Ansible will successfully create the requested ASG and
immediately afterwards fail with the following error:

```
TASK: [cluster_asg | ec2_asg ] *************************************************
failed: [cluster_asg] => {"failed": true, "parsed": false}
Traceback (most recent call last):
  File "/home/moe/.ansible/tmp/ansible-tmp-1422991199.84-118626399276493/ec2_asg", line 2324, in <module>
    main()
  File "/home/moe/.ansible/tmp/ansible-tmp-1422991199.84-118626399276493/ec2_asg", line 2314, in main
    create_changed, asg_properties=create_autoscaling_group(connection, module)
  File "/home/moe/.ansible/tmp/ansible-tmp-1422991199.84-118626399276493/ec2_asg", line 2054, in create_autoscaling_group
    existing_tag_map = dict((tag.key, tag) for tag in existing_tags)
  File "/home/moe/.ansible/tmp/ansible-tmp-1422991199.84-118626399276493/ec2_asg", line 2054, in <genexpr>
    existing_tag_map = dict((tag.key, tag) for tag in existing_tags)
AttributeError: 'dict' object has no attribute 'key'
```
##### Case 2: At least one tagged ASG exists on AWS already

Ansible will fail with the following error:

```
TASK: [cluster_asg | ec2_asg ] *************************************************
failed: [cluster_asg] => {"failed": true, "parsed": false}
Traceback (most recent call last):
  File "/home/moe/.ansible/tmp/ansible-tmp-1422990578.86-201224704818227/ec2_asg", line 2324, in <module>
    main()
  File "/home/moe/.ansible/tmp/ansible-tmp-1422990578.86-201224704818227/ec2_asg", line 2322, in main
    module.exit_json( changed = changed, **asg_properties )
  File "/home/moe/.ansible/tmp/ansible-tmp-1422990578.86-201224704818227/ec2_asg", line 1388, in exit_json
    print self.jsonify(kwargs)
  File "/home/moe/.ansible/tmp/ansible-tmp-1422990578.86-201224704818227/ec2_asg", line 1366, in jsonify
    return json.dumps(data)
  File "/home/moe/code/px/sys/python/lib/python2.7/json/__init__.py", line 243, in dumps
    return _default_encoder.encode(obj)
  File "/home/moe/code/px/sys/python/lib/python2.7/json/encoder.py", line 207, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/home/moe/code/px/sys/python/lib/python2.7/json/encoder.py", line 270, in iterencode
    return _iterencode(o, 0)
  File "/home/moe/code/px/sys/python/lib/python2.7/json/encoder.py", line 184, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: Tag(cluster=crunch2) is not JSON serializable
```

Case 2 looks similar to https://github.com/ansible/ansible-modules-core/issues/274



Discussion:

Resolving patch:
diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py
--- a/cloud/amazon/ec2_asg.py
+++ b/cloud/amazon/ec2_asg.py
@@ -224,6 +224,16 @@ def enforce_required_arguments(module):
 
 def get_properties(autoscaling_group):
     properties = dict((attr, getattr(autoscaling_group, attr)) for attr in ASG_ATTRIBUTES)
+
+    # Ugly hack to make this JSON-serializable.  We take a list of boto Tag
+    # objects and replace them with a dict-representation.  Needed because the
+    # tags are included in ansible's return value (which is jsonified)
+    if 'tags' in properties and isinstance(properties['tags'], list):
+        serializable_tags = {}
+        for tag in properties['tags']:
+            serializable_tags[tag.key] = [tag.value, tag.propagate_at_launch]
+        properties['tags'] = serializable_tags
+
     properties['healthy_instances'] = 0
     properties['in_service_instances'] = 0
     properties['unhealthy_instances'] = 0
@@ -326,18 +336,12 @@ def create_autoscaling_group(connection, module):
 
     asg_tags = []
     for tag in set_tags:
-        if tag.has_key('key') and tag.has_key('value'):  # this block is to support depricated form
-            asg_tags.append(Tag(key=tag.get('key'),
-                value=tag.get('value'),
-                propagate_at_launch=bool(tag.get('propagate_at_launch', True)),
-                resource_id=group_name))
-        else:
-            for k,v in tag.iteritems():
-                if k !='propagate_at_launch':
-                    asg_tags.append(Tag(key=k,
-                         value=v,
-                         propagate_at_launch=bool(tag.get('propagate_at_launch', True)),
-                         resource_id=group_name))
+        for k,v in tag.iteritems():
+            if k !='propagate_at_launch':
+                asg_tags.append(Tag(key=k,
+                     value=v,
+                     propagate_at_launch=bool(tag.get('propagate_at_launch', True)),
+                     resource_id=group_name))
 
     if not as_groups:
         if not vpc_zone_identifier and not availability_zones:
@@ -393,30 +397,25 @@ def create_autoscaling_group(connection, module):
                     setattr(as_group, attr, module_attr)
 
         if len(set_tags) > 0:
-            existing_tags = as_group.tags
-            existing_tag_map = dict((tag.key, tag) for tag in existing_tags)
-            for tag in set_tags:
-                if tag.has_key('key') and tag.has_key('value'):  # this is to support deprecated method
-                    if 'key' not in tag:
-                        continue
-                    if ( not tag['key'] in existing_tag_map or
-                        existing_tag_map[tag['key']].value != tag['value'] or
-                        ('propagate_at_launch' in tag and
-                        existing_tag_map[tag['key']].propagate_at_launch != tag['propagate_at_launch']) ):
-                        changed = True
-                        continue
-                else:
-                    for k,v in tag.iteritems():
-                        if k !='propagate_at_launch':
-                            if ( not k in existing_tag_map or
-                                existing_tag_map[k].value != v or
-                                ('propagate_at_launch' in tag and
-                                existing_tag_map[k].propagate_at_launch != tag['propagate_at_launch']) ):
-                                changed = True
-                                continue
-            if changed:
+            have_tags = {}
+            want_tags = {}
+
+            for tag in asg_tags:
+                want_tags[tag.key] = [tag.value, tag.propagate_at_launch]
+
+            dead_tags = []
+            for tag in as_group.tags:
+                have_tags[tag.key] = [tag.value, tag.propagate_at_launch]
+                if not tag.key in want_tags:
+                    changed = True
+                    dead_tags.append(tag)
+
+            if dead_tags != []:
+                connection.delete_tags(dead_tags)
+
+            if have_tags != want_tags:
+                changed = True
                 connection.create_or_update_tags(asg_tags)
-                as_group.tags = asg_tags
 
         # handle loadbalancers separately because None != []
         load_balancers = module.params.get('load_balancers') or []


Test patch: