Repository: ansible/ansible

Issue:
junos_config does not handle multiple delete statements correctly
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
modules/network/junos/junos_config.py
module_utils/junos.py

##### ANSIBLE VERSION
```
ansible 2.3.0.0
  config file = 
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Dec 17 2016, 23:03:43) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
```

##### CONFIGURATION
```
[defaults]
inventory = ./hosts
transport = ssh
roles_path = ./roles
remote_user = ansible
hash_behaviour = merge
ask_vault_pass = True
display_skipped_hosts = False
retry_files_enabled = False

[ssh_connection]
pipelining = True
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY
The `filter_delete_statements` function in [modules/network/junos/junos_config.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/junos/junos_config.py) removes `delete` statements from the change list if they don't have a matching `set` statement in the running configuration on the router. However, it deletes from the list in ascending order, causing the indexes of subsequent lines to change when deleting a line. This causes an IndexError when deleting or even to delete the wrong lines from the change-set.

In addition to that it seems that JunOS can't handle empty change-sets, for example when `filter_delete_statements` only contains delete statements and all statements are removed because the corresponding `set` statements have already been deleted in a previous run.

##### STEPS TO REPRODUCE
```yaml
- name: "disable load-balancing"
  junos_config:
    lines:
    - "delete policy-options policy-statement load-balance"
    - "delete routing-options forwarding-table export load-balance"
```

When the specified statements are both already absent in the configuration then an `IndexError: list assignment index out of range` is raised.

```yaml
- name: "disable load-balancing"
  junos_config:
    lines:
    - "delete policy-options policy-statement load-balance"
```

If the specified statement is already absent in the configuration then the resulting change-set is empty and an `[Errno 61] Connection refused"` is raised. This is caused by the empty change-set.

##### EXPECTED RESULTS
Both cases should just give an `ok` response as the configuration is already conforming to the specified commands.

##### ACTUAL RESULTS
When removing multiple `delete` lines:
```
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: IndexError: list assignment index out of range
fatal: [vmx1.retevia.eu]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Traceback (most recent call last):\n  File \"/var/folders/xs/nwtp76d53vv5_4mvq3cyfsrh0000gn/T/ansible__wWeAO/ansible_module_junos_config.py\", line 342, in <module>\n    main()\n  File \"/var/folders/xs/nwtp76d53vv5_4mvq3cyfsrh0000gn/T/ansible__wWeAO/ansible_module_junos_config.py\", line 332, in main\n    diff = configure_device(module, warnings)\n  File \"/var/folders/xs/nwtp76d53vv5_4mvq3cyfsrh0000gn/T/ansible__wWeAO/ansible_module_junos_config.py\", line 264, in configure_device\n    candidate = filter_delete_statements(module, candidate)\n  File \"/var/folders/xs/nwtp76d53vv5_4mvq3cyfsrh0000gn/T/ansible__wWeAO/ansible_module_junos_config.py\", line 231, in filter_delete_statements\n    del modified_candidate[index]\nIndexError: list assignment index out of range\n", "module_stdout": "", "msg": "MODULE FAILURE", "rc": 1}
```

When sending an empty change-set:
```
fatal: [router]: FAILED! => {
    "changed": false, 
    "err": "[Errno 61] Connection refused", 
    "failed": true, 
    "invocation": {
        "module_args": {
            "backup": false, 
            "comment": "configured by junos_config", 
            "confirm": 0, 
            "host": null, 
            "lines": [
                "delete policy-options policy-statement load-balance", 
            ], 
            "password": null, 
            "port": null, 
            "provider": null, 
            "replace": null, 
            "rollback": null, 
            "src": null, 
            "src_format": null, 
            "ssh_keyfile": null, 
            "timeout": 10, 
            "transport": null, 
            "update": "merge", 
            "username": null, 
            "zeroize": false
        }
    }, 
    "msg": "unable to connect to socket"
}
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/module_utils/junos.py b/lib/ansible/module_utils/junos.py
--- a/lib/ansible/module_utils/junos.py
+++ b/lib/ansible/module_utils/junos.py
@@ -169,6 +169,9 @@ def get_diff(module):
 def load_config(module, candidate, warnings, action='merge', commit=False, format='xml',
                 comment=None, confirm=False, confirm_timeout=None):
 
+    if not candidate:
+        return
+
     with locked_config(module):
         if isinstance(candidate, list):
             candidate = '\n'.join(candidate)
diff --git a/lib/ansible/modules/network/junos/junos_config.py b/lib/ansible/modules/network/junos/junos_config.py
--- a/lib/ansible/modules/network/junos/junos_config.py
+++ b/lib/ansible/modules/network/junos/junos_config.py
@@ -242,7 +242,7 @@ def filter_delete_statements(module, candidate):
     config = to_native(match.text, encoding='latin1')
 
     modified_candidate = candidate[:]
-    for index, line in enumerate(candidate):
+    for index, line in reversed(list(enumerate(candidate))):
         if line.startswith('delete'):
             newline = re.sub('^delete', 'set', line)
             if newline not in config:


Test patch: