Repository: ansible/ansible

Issue:
iam module does not support deleting user with mfa activated
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
lib/ansible/modules/cloud/amazon/iam.py

##### ANSIBLE VERSION
```
ansible 2.3.1.0
  config file = 
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Jul 18 2017, 09:17:00) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)]
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY
Cannot delete a user who has MFA activated.

##### STEPS TO REPRODUCE
Create an AWS IAM user, activate MFA for that user, then try to delete the user.

```yaml
  - name: Delete disabled users
    iam:
      iam_type: user
      name: user1
      state: absent
```

##### EXPECTED RESULTS
The user should have been deleted 

##### ACTUAL RESULTS
The user is not being deleted, although the login profile is being removed.  Ansible fails with the following error:

TASK [iam-users : Delete disabled users] ***************************************
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "delete failed BotoServerError: 409 Conflict\n<ErrorResponse xmlns=\"https://iam.amazonaws.com/doc/2010-05-08/\">\n  <Error>\n    <Type>Sender</Type>\n    <Code>DeleteConflict</Code>\n    <Message>Cannot delete entity, must delete MFA device first.</Message>\n  </Error>\n  <RequestId>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</RequestId>\n</ErrorResponse>\n"}

iam module does not support deleting user with mfa activated
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
lib/ansible/modules/cloud/amazon/iam.py

##### ANSIBLE VERSION
```
ansible 2.3.1.0
  config file = 
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Jul 18 2017, 09:17:00) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)]
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY
Cannot delete a user who has MFA activated.

##### STEPS TO REPRODUCE
Create an AWS IAM user, activate MFA for that user, then try to delete the user.

```yaml
  - name: Delete disabled users
    iam:
      iam_type: user
      name: user1
      state: absent
```

##### EXPECTED RESULTS
The user should have been deleted 

##### ACTUAL RESULTS
The user is not being deleted, although the login profile is being removed.  Ansible fails with the following error:

TASK [iam-users : Delete disabled users] ***************************************
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "delete failed BotoServerError: 409 Conflict\n<ErrorResponse xmlns=\"https://iam.amazonaws.com/doc/2010-05-08/\">\n  <Error>\n    <Type>Sender</Type>\n    <Code>DeleteConflict</Code>\n    <Message>Cannot delete entity, must delete MFA device first.</Message>\n  </Error>\n  <RequestId>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</RequestId>\n</ErrorResponse>\n"}



Discussion:
cc @defionscode @seiffert
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @defionscode @seiffert
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/iam.py b/lib/ansible/modules/cloud/amazon/iam.py
--- a/lib/ansible/modules/cloud/amazon/iam.py
+++ b/lib/ansible/modules/cloud/amazon/iam.py
@@ -244,42 +244,60 @@ def create_user(module, iam, name, pwd, path, key_state, key_count):
         return (user_info, changed)
 
 
-def delete_user(module, iam, name):
+def delete_dependencies_first(module, iam, name):
     changed = False
+    # try to delete any keys
     try:
         current_keys = [ck['access_key_id'] for ck in
             iam.get_all_access_keys(name).list_access_keys_result.access_key_metadata]
         for key in current_keys:
             iam.delete_access_key(key, name)
-        try:
-            login_profile = iam.get_login_profiles(name).get_login_profile_response
-        except boto.exception.BotoServerError as err:
-            error_msg = boto_exception(err)
-            if ('Cannot find Login Profile') in error_msg:
-                iam.delete_user(name)
-        else:
-            iam.delete_login_profile(name)
-            iam.delete_user(name)
-    except Exception as ex:
-        module.fail_json(changed=False, msg="delete failed %s" %ex)
-        if ('must detach all policies first') in error_msg:
-            for policy in iam.get_all_user_policies(name).list_user_policies_result.policy_names:
-                iam.delete_user_policy(name, policy)
-            try:
-                iam.delete_user(name)
-            except boto.exception.BotoServerError as err:
-                error_msg = boto_exception(err)
-                if ('must detach all policies first') in error_msg:
-                    module.fail_json(changed=changed, msg="All inline polices have been removed. Though it appears"
-                                                          "that %s has Managed Polices. This is not "
-                                                          "currently supported by boto. Please detach the polices "
-                                                          "through the console and try again." % name)
-                else:
-                    module.fail_json(changed=changed, msg=str(error_msg))
-            else:
-                changed = True
-        else:
-            module.fail_json(changed=changed, msg=str(error_msg))
+        changed = True
+    except boto.exception.BotoServerError as err:
+        module.fail_json(changed=changed, msg="Failed to delete keys: %s" % err, exception=traceback.format_exc())
+
+    # try to delete login profiles
+    try:
+        login_profile = iam.get_login_profiles(name).get_login_profile_response
+        iam.delete_login_profile(name)
+        changed = True
+    except boto.exception.BotoServerError as err:
+        error_msg = boto_exception(err)
+        if 'Cannot find Login Profile' not in error_msg:
+            module.fail_json(changed=changed, msg="Failed to delete login profile: %s" % err, exception=traceback.format_exc())
+
+    # try to detach policies
+    try:
+        for policy in iam.get_all_user_policies(name).list_user_policies_result.policy_names:
+            iam.delete_user_policy(name, policy)
+        changed = True
+    except boto.exception.BotoServerError as err:
+        error_msg = boto_exception(err)
+        if 'must detach all policies first' in error_msg:
+            module.fail_json(changed=changed, msg="All inline polices have been removed. Though it appears"
+                                                  "that %s has Managed Polices. This is not "
+                                                  "currently supported by boto. Please detach the polices "
+                                                  "through the console and try again." % name)
+        module.fail_json(changed=changed, msg="Failed to delete policies: %s" % err, exception=traceback.format_exc())
+
+    # try to deactivate associated MFA devices
+    try:
+        mfa_devices = iam.get_all_mfa_devices(name).get('list_mfa_devices_response', {}).get('list_mfa_devices_result', {}).get('mfa_devices', [])
+        for device in mfa_devices:
+            iam.deactivate_mfa_device(name, device['serial_number'])
+        changed = True
+    except boto.exception.BotoServerError as err:
+        module.fail_json(changed=changed, msg="Failed to deactivate associated MFA devices: %s" % err, exception=traceback.format_exc())
+
+    return changed
+
+
+def delete_user(module, iam, name):
+    changed = delete_dependencies_first(module, iam, name)
+    try:
+        iam.delete_user(name)
+    except boto.exception.BotoServerError as ex:
+        module.fail_json(changed=changed, msg="Failed to delete user %s: %s" % (name, ex), exception=traceback.format_exc())
     else:
         changed = True
     return name, changed


Test patch: