Repository: ansible/ansible

Issue:
REGRESSION: ios_facts: return type changed from string to list (stacked switches)
##### ISSUE TYPE
 - Bug report

##### COMPONENT NAME
ios_facts

##### ANSIBLE VERSION
```
2.5.0
```

##### SUMMARY
https://github.com/ansible/ansible/pull/33714 introduced a change to return details of the stacked switches. This is definitely useful data to return, though we've changed the format of the data returned

This needs changing, so that:
* `ansible_net_model` and `ansible_net_serialnum` need changing back to be strings, representing the main system, as they were in Ansible 2.4
* New facts should be created for the stacks. These will need adding to the `RETURN` documentation block in the module

I'm not sure if there should be a dict for stacked that contains elements for each stacked switch, or something else

PR which introduced this issue: https://github.com/ansible/ansible/pull/33714

See https://github.com/ansible/ansible/issues/34768 for the discussion that lead to this bug being raised.

*NOTE* when fixing this issue we need to ensure that **ios-xe** works as per the original issue in https://github.com/ansible/ansible/issues/34768#issuecomment-358787000


Discussion:
@kalimsshar @Qalthos FYI
cc @kedarX @privateip @rcarrillocruz
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Also may want to look at `show inventory` vs `show version`

Resolving patch:
diff --git a/lib/ansible/modules/network/ios/ios_facts.py b/lib/ansible/modules/network/ios/ios_facts.py
--- a/lib/ansible/modules/network/ios/ios_facts.py
+++ b/lib/ansible/modules/network/ios/ios_facts.py
@@ -49,33 +49,19 @@
 """
 
 EXAMPLES = """
-# Note: examples below use the following provider dict to handle
-#       transport and authentication to the node.
----
-vars:
-  cli:
-    host: "{{ inventory_hostname }}"
-    username: cisco
-    password: cisco
-    transport: cli
-
----
 # Collect all facts from the device
 - ios_facts:
     gather_subset: all
-    provider: "{{ cli }}"
 
 # Collect only the config and default facts
 - ios_facts:
     gather_subset:
       - config
-    provider: "{{ cli }}"
 
 # Do not collect hardware facts
 - ios_facts:
     gather_subset:
       - "!hardware"
-    provider: "{{ cli }}"
 """
 
 RETURN = """
@@ -105,6 +91,14 @@
   description: The image file the device is running
   returned: always
   type: string
+ansible_net_stacked_models:
+  description: The model names of each device in the stack
+  returned: when multiple devices are configured in a stack
+  type: list
+ansible_net_stacked_serialnums:
+  description: The serial numbers of each device in the stack
+  returned: when multiple devices are configured in a stack
+  type: list
 
 # hardware
 ansible_net_filesystems:
@@ -163,10 +157,10 @@ def __init__(self, module):
         self.responses = None
 
     def populate(self):
-        self.responses = run_commands(self.module, self.COMMANDS, check_rc=False)
+        self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False)
 
     def run(self, cmd):
-        return run_commands(self.module, cmd, check_rc=False)
+        return run_commands(self.module, commands=cmd, check_rc=False)
 
 
 class Default(FactsBase):
@@ -182,6 +176,7 @@ def populate(self):
             self.facts['model'] = self.parse_model(data)
             self.facts['image'] = self.parse_image(data)
             self.facts['hostname'] = self.parse_hostname(data)
+            self.parse_stacks(data)
 
     def parse_version(self, data):
         match = re.search(r'Version (\S+?)(?:,\s|\s)', data)
@@ -194,13 +189,9 @@ def parse_hostname(self, data):
             return match.group(1)
 
     def parse_model(self, data):
-        match = re.findall(r'^Model number\s+: (\S+)', data, re.M)
+        match = re.search(r'^[Cc]isco (\S+).+bytes of .*memory', data, re.M)
         if match:
-            return match
-        else:
-            match = re.search(r'^[Cc]isco (\S+).+bytes of memory', data, re.M)
-            if match:
-                return [match.group(1)]
+            return match.group(1)
 
     def parse_image(self, data):
         match = re.search(r'image file is "(.+)"', data)
@@ -208,13 +199,18 @@ def parse_image(self, data):
             return match.group(1)
 
     def parse_serialnum(self, data):
+        match = re.search(r'board ID (\S+)', data)
+        if match:
+            return match.group(1)
+
+    def parse_stacks(self, data):
+        match = re.findall(r'^Model number\s+: (\S+)', data, re.M)
+        if match:
+            self.facts['stacked_models'] = match
+
         match = re.findall(r'^System serial number\s+: (\S+)', data, re.M)
         if match:
-            return match
-        else:
-            match = re.search(r'board ID (\S+)', data)
-            if match:
-                return [match.group(1)]
+            self.facts['stacked_serialnums'] = match
 
 
 class Hardware(FactsBase):


Test patch:
diff --git a/test/units/modules/network/ios/fixtures/ios_facts_show_version b/test/units/modules/network/ios/fixtures/ios_facts_show_version
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/ios/fixtures/ios_facts_show_version
@@ -0,0 +1,68 @@
+Cisco Internetwork Operating System Software
+IOS (tm) C3750 Software (C3750-I5-M), Version 12.1(14)EA1, RELEASE SOFTWARE (fc1)
+Copyright (c) 1986-2003 by cisco Systems, Inc.
+Compiled Tue 22-Jul-03 13:17 by antonino
+Image text-base: 0x00003000, data-base: 0x008F0CF8
+
+ROM: Bootstrap program is C3750 boot loader
+BOOTLDR: C3750 Boot Loader (C3750-HBOOT-M) Version 12.1(11r)AX, RELEASE SOFTWARE (fc1)
+
+3750RJ uptime is 1 hour, 29 minutes
+System returned to ROM by power-on
+System image file is "flash:c3750-i5-mz.121.14-EA1/c3750-i5-mz.121.14-EA1.bin"
+
+cisco WS-C3750-24TS (PowerPC405) processor (revision A0) with 120822K/10240K bytes of memory.
+Processor board ID CAT0726R0ZU
+Last reset from power-on
+Bridging software.
+2 Virtual Ethernet/IEEE 802.3 interface(s)
+48 FastEthernet/IEEE 802.3 interface(s)
+16 Gigabit Ethernet/IEEE 802.3 interface(s)
+The password-recovery mechanism is enabled.
+
+512K bytes of flash-simulated non-volatile configuration memory.
+Base ethernet MAC Address : 00:0D:29:B4:18:00
+Motherboard assembly number : 73-7055-06
+Power supply part number : 341-0034-01
+Motherboard serial number : CAT0726043V
+Power supply serial number : PHI0708009K
+Model revision number : A0
+Motherboard revision number : A0
+Model number : WS-C3750-24TS-E
+System serial number : CAT0726R0ZU
+
+Switch Ports Model SW Version SW Image
+------ ----- ----- ---------- ----------
+* 1 26 WS-C3750-24TS 12.1(14)EA1 C3750-I5-M
+2 26 WS-C3750-24TS 12.1(14)EA1 C3750-I5-M
+3 12 WS-C3750G-12S 12.1(14)EA1 C3750-I5-M
+
+Switch 02
+---------
+Switch Uptime : 1 hour, 29 minutes
+Base ethernet MAC Address : 00:0D:29:B4:3F:00
+Motherboard assembly number : 73-7055-06
+Power supply part number : 341-0034-01
+Motherboard serial number : CAT07260438
+Power supply serial number : PHI0708008X
+Model revision number : A0
+Motherboard revision number : A0
+Model number : WS-C3750-24TS-E
+System serial number : CAT0726R10A
+
+Switch 03
+---------
+Switch Uptime : 1 hour, 29 minutes
+Base ethernet MAC Address : 00:0D:BD:6A:3E:00
+Motherboard assembly number : 73-8307-06
+Power supply part number : 341-0048-01
+Motherboard serial number : CAT073205S2
+Power supply serial number : DTH0731055Z
+Model revision number : A0
+Motherboard revision number : A0
+Model number : WS-C3750G-12S-E
+System serial number : CAT0732R0M4
+Top assembly part number : 800-23419-01
+Top assembly revision number : A0
+
+Configuration register is 0xF
diff --git a/test/units/modules/network/ios/test_ios_facts.py b/test/units/modules/network/ios/test_ios_facts.py
new file mode 100644
--- /dev/null
+++ b/test/units/modules/network/ios/test_ios_facts.py
@@ -0,0 +1,66 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.compat.tests.mock import patch
+from ansible.modules.network.ios import ios_facts
+from units.modules.utils import set_module_args
+from .ios_module import TestIosModule, load_fixture
+
+
+class TestIosFactsModule(TestIosModule):
+
+    module = ios_facts
+
+    def setUp(self):
+        super(TestIosFactsModule, self).setUp()
+        self.mock_run_commands = patch('ansible.modules.network.ios.ios_facts.run_commands')
+        self.run_commands = self.mock_run_commands.start()
+
+    def tearDown(self):
+        super(TestIosFactsModule, self).tearDown()
+        self.mock_run_commands.stop()
+
+    def load_fixtures(self, commands=None):
+        def load_from_file(*args, **kwargs):
+            module = args
+            commands = kwargs['commands']
+            output = list()
+
+            for command in commands:
+                filename = str(command).split(' | ')[0].replace(' ', '_')
+                output.append(load_fixture('ios_facts_%s' % filename))
+            return output
+
+        self.run_commands.side_effect = load_from_file
+
+    def test_ios_facts_stacked(self):
+        set_module_args(dict(gather_subset='default'))
+        result = self.execute_module()
+        self.assertEqual(
+            result['ansible_facts']['ansible_net_model'], 'WS-C3750-24TS'
+        )
+        self.assertEqual(
+            result['ansible_facts']['ansible_net_serialnum'], 'CAT0726R0ZU'
+        )
+        self.assertEqual(
+            result['ansible_facts']['ansible_net_stacked_models'], ['WS-C3750-24TS-E', 'WS-C3750-24TS-E', 'WS-C3750G-12S-E']
+        )
+        self.assertEqual(
+            result['ansible_facts']['ansible_net_stacked_serialnums'], ['CAT0726R0ZU', 'CAT0726R10A', 'CAT0732R0M4']
+        )