Repository: ansible/ansible

Issue:
ios_config 'save' option was removed in 2.4 still has deprecation as only a warning
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
ios_config

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.0.0
  config file = /data/conf/netdev/plays/ansible.cfg
  configured module search path = [u'/data/conf/netdev/extensions/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### SUMMARY

Ansible 2.4 has a deprecation warning for the 'save' option being removed in 2.4.  Since it has been removed, this should likely be a fatal error using 'save' in the play.

##### STEPS TO REPRODUCE
Run the below play

<!--- Paste example playbooks or commands between quotes below -->
```yaml
  - name: save test
    ios_config:
      authorize: yes
      save: yes
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
Fatal error message on invalid ios_config save option.

##### ACTUAL RESULTS
```
[DEPRECATION WARNING]: Param 'save' is deprecated. See the module docs for more information. This feature will be removed in version 2.4. Deprecation warnings  can be disabled by setting deprecation_warnings=False in ansible.cfg.

```
##### Other References
Fwiw, this has been mentioned in another ticket https://github.com/ansible/ansible/pull/32744#issuecomment-344660626

@gundalow also asked me to open a issue to see if this could slide into the 2.4.2 release.

Hoping the the save_now prompting fix also is included #32619 


Discussion:
cc @Qalthos @ganeshrn @kedarX @privateip @rcarrillocruz @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@termlen0 just fyi regarding your comment I referenced above.  I reread it and noted one thing.  You mentioned that save works in 2.4.  **It does not**, it is a NOOP, at least in the 2.4.0 code in the centos RPM ansible-2.4.0.0-5.el7.noarch

It looks to me as if 'save' silently stopped working as soon as 'save_when' was added, but I could be mistaken.
https://github.com/ansible/ansible/commit/0b6f0e6c0d95e271cf4173b28ece4a5e58902f9c#diff-0ca42d7f4ac4dd7fb03025f0189851f6L332
@bdowling You are correct. `save` pretends to work - which is even more alarming!

Resolving patch:
diff --git a/lib/ansible/modules/network/ios/ios_config.py b/lib/ansible/modules/network/ios/ios_config.py
--- a/lib/ansible/modules/network/ios/ios_config.py
+++ b/lib/ansible/modules/network/ios/ios_config.py
@@ -181,10 +181,12 @@
         will only be copied to the startup-config if it has changed since
         the last save to startup-config.  If the argument is set to
         I(never), the running-config will never be copied to the
-        startup-config
+        startup-config.  If the argument is set to I(changed), then the running-config
+        will only be copied to the startup-config if the task has made a change.
+        I(changed) was added in Ansible 2.5.
     required: false
     default: never
-    choices: ['always', 'never', 'modified']
+    choices: ['always', 'never', 'modified', 'changed']
     version_added: "2.4"
   diff_against:
     description:
@@ -366,6 +368,16 @@ def get_candidate(module):
     return candidate, banners
 
 
+def save_config(module, result):
+    result['changed'] = True
+    if not module.check_mode:
+        run_commands(module, 'copy running-config startup-config\r')
+    else:
+        module.warn('Skipping command `copy running-config startup-config` '
+                    'due to check_mode.  Configuration not copied to '
+                    'non-volatile storage')
+
+
 def main():
     """ main entry point for module execution
     """
@@ -388,7 +400,7 @@ def main():
         defaults=dict(type='bool', default=False),
         backup=dict(type='bool', default=False),
 
-        save_when=dict(choices=['always', 'never', 'modified'], default='never'),
+        save_when=dict(choices=['always', 'never', 'modified', 'changed'], default='never'),
 
         diff_against=dict(choices=['startup', 'intended', 'running']),
         diff_ignore_lines=dict(type='list'),
@@ -474,20 +486,18 @@ def main():
 
     diff_ignore_lines = module.params['diff_ignore_lines']
 
-    if module.params['save_when'] != 'never':
+    if module.params['save_when'] == 'always' or module.params['save']:
+        save_config(module, result)
+    elif module.params['save_when'] == 'modified':
         output = run_commands(module, ['show running-config', 'show startup-config'])
 
         running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines)
         startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines)
 
-        if running_config.sha1 != startup_config.sha1 or module.params['save_when'] == 'always':
-            result['changed'] = True
-            if not module.check_mode:
-                run_commands(module, 'copy running-config startup-config\r')
-            else:
-                module.warn('Skipping command `copy running-config startup-config` '
-                            'due to check_mode.  Configuration not copied to '
-                            'non-volatile storage')
+        if running_config.sha1 != startup_config.sha1:
+            save_config(module, result)
+    elif module.params['save_when'] == 'changed' and result['changed']:
+        save_config(module, result)
 
     if module._diff:
         if not running_config:


Test patch:
diff --git a/test/units/modules/network/ios/test_ios_config.py b/test/units/modules/network/ios/test_ios_config.py
--- a/test/units/modules/network/ios/test_ios_config.py
+++ b/test/units/modules/network/ios/test_ios_config.py
@@ -74,7 +74,35 @@ def test_ios_config_save_always(self):
         self.run_commands.return_value = "Hostname foo"
         set_module_args(dict(save_when='always'))
         self.execute_module(changed=True)
-        self.assertEqual(self.run_commands.call_count, 2)
+        self.assertEqual(self.run_commands.call_count, 1)
+        self.assertEqual(self.get_config.call_count, 0)
+        self.assertEqual(self.load_config.call_count, 0)
+        args = self.run_commands.call_args[0][1]
+        self.assertIn('copy running-config startup-config\r', args)
+
+    def test_ios_config_save_changed_true(self):
+        src = load_fixture('ios_config_src.cfg')
+        set_module_args(dict(src=src, save_when='changed'))
+        commands = ['hostname foo', 'interface GigabitEthernet0/0', 'no ip address']
+        self.execute_module(changed=True, commands=commands)
+        self.assertEqual(self.run_commands.call_count, 1)
+        self.assertEqual(self.get_config.call_count, 1)
+        self.assertEqual(self.load_config.call_count, 1)
+        args = self.run_commands.call_args[0][1]
+        self.assertIn('copy running-config startup-config\r', args)
+
+    def test_aruba_config_save_changed_false(self):
+        set_module_args(dict(save_when='changed'))
+        self.execute_module(changed=False)
+        self.assertEqual(self.run_commands.call_count, 0)
+        self.assertEqual(self.get_config.call_count, 0)
+        self.assertEqual(self.load_config.call_count, 0)
+
+    def test_ios_config_save(self):
+        self.run_commands.return_value = "hostname foo"
+        set_module_args(dict(save=True))
+        self.execute_module(changed=True)
+        self.assertEqual(self.run_commands.call_count, 1)
         self.assertEqual(self.get_config.call_count, 0)
         self.assertEqual(self.load_config.call_count, 0)
         args = self.run_commands.call_args[0][1]