Repository: ansible/ansible-modules-core

Issue:
ios_facts: gather_facts :hardware crash on cisco switches
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

ios_facts: gather_facts : hardware 
##### ANSIBLE VERSION

```
ansible 2.2.0.0 (stable-2.2 b702d3810e)
```
##### OS / ENVIRONMENT

<!---
N\A
-->
##### SUMMARY

Looks like the process to check the memory on the ios device uses "show memory statistics | include Processor". This command is not available on the Cisco switches and makes the module fail. Tested with Cisco 3850 switches.
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->

```
   vars:
      cli:
        timeout: 15
        host: "{{ inventory_hostname }}"
        username: "{{ creds_sw['username'] }}"
        password: "{{ creds_sw['password'] }}"
        auth_pass: "{{ creds_sw['auth_pass'] }}"
        transport: cli
   tasks:
         - name: Get login info
           include_vars: secret.yml

     - name: get facts
           ignore_errors: yes
           ios_facts:
              authorize: yes
              provider: "{{ cli }}"
              gather_subset:
                - "hardware"

```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->
##### ACTUAL RESULTS

An exception occurred during task execution. To see the full traceback, use -vvv. The error was: <Hostname>#
fatal: [hostname]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_p2ys4P/ansible_module_ios_facts.py\", line 461, in <module>\n    main()\n  File \"/tmp/ansible_p2ys4P/ansible_module_ios_facts.py\", line 443, in main\n    runner.run()\n  File \"/tmp/ansible_p2ys4P/ansible_modlib.zip/ansible/module_utils/netcli.py\", line 170, in run\n  File \"/tmp/ansible_p2ys4P/ansible_modlib.zip/ansible/module_utils/netcli.py\", line 98, in run_commands\n  File \"/tmp/ansible_p2ys4P/ansible_modlib.zip/ansible/module_utils/shell.py\", line 257, in run_commands\n  File \"/tmp/ansible_p2ys4P/ansible_modlib.zip/ansible/module_utils/shell.py\", line 254, in execute\nansible.module_utils.network.NetworkError: matched error in response:  statistics | include Process         r\r\n**_show memory statistics | include Processor_**\r\n             ^\r\n% Invalid input detected at '^' marker.\r\n\r\nhostname#\n", "module_stdout": "", "msg": "MODULE FAILURE"}

```

```



Discussion:
<!-- @boudreau: Thanks for filing this bug report! -->

@privateip, @gundalow, ping. This issue is waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->

I tried this as well and even for me show memory statistics | include Processor is not working. I used v3650 so this seems to be a problem for multiple IOS devices. 

Here is the output when I run the command directly on the device.

```
show memory statistics | include Processor
                              ^
% Invalid input detected at '^' marker.

```

Update:

I hashed below two entries in file ansible/lib/ansible/modules/core/network/ios/ios_facts.py and now ios_facts seems to work fine.

```
add_command(self.runner, 'show memory statistics | include Processor')
data = self.runner.get_command('show memory statistics | include Processor')

```

Variables published by default were (some of these were null or did not return expected values):
ansible_net_all_ipv4_addresses
ansible_net_all_ipv6_addresses
ansible_net_config
ansible_net_filesystems
ansible_net_gather_subset
ansible_net_hostname
ansible_net_image
ansible_net_interfaces
ansible_net_model -- This returned null
ansible_net_serialnum
ansible_net_version -- This did not return the exact version.

I'm still reading through code to check how I can optimize it to display correct OS version and model number for starters as that can help me generate a great inventory list.

Note - I did "gather_subset: all", registered output to a variable and published variable using debug to check what all is being returned.

on the router side the command does : 

```
sh memory statistics
                Head    Total(b)     Used(b)     Free(b)   Lowest(b)  Largest(b)
Processor   1171E760   1531906540   363084632   1168821908   1144403784   597311580
      I/O   40000000   268435456   133995684   134439772   134109576   122976156
```

and on the switch side we could use "show memory | include Process" ?:

```
sh memory
System memory   : 3937300K total, 1619112K used, 2318188K free, 222060K kernel reserved
Lowest(b)       : 1672667524

                Total(K)     Used(K)      Free(K)
Process         3937300      1619112      2318188
Config          0            0            0
```


Resolving patch:
diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py
--- a/network/ios/ios_facts.py
+++ b/network/ios/ios_facts.py
@@ -42,19 +42,31 @@
 """
 
 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 = """
@@ -127,44 +139,35 @@
 import itertools
 
 import ansible.module_utils.ios
-from ansible.module_utils.netcli import CommandRunner, AddCommandError
 from ansible.module_utils.network import NetworkModule
 from ansible.module_utils.six import iteritems
 from ansible.module_utils.six.moves import zip
 
 
-def add_command(runner, command):
-    try:
-        runner.add_command(command)
-    except AddCommandError:
-        # AddCommandError is raised for any issue adding a command to
-        # the runner.  Silently ignore the exception in this case
-        pass
-
 class FactsBase(object):
 
-    def __init__(self, runner):
-        self.runner = runner
+    def __init__(self, module):
+        self.module = module
         self.facts = dict()
+        self.failed_commands = list()
 
-        self.commands()
+    def run(self, cmd):
+        try:
+            return self.module.cli(cmd)[0]
+        except:
+            self.failed_commands.append(cmd)
 
-    def commands(self):
-        raise NotImplementedError
 
 class Default(FactsBase):
 
-    def commands(self):
-        add_command(self.runner, 'show version')
-
     def populate(self):
-        data = self.runner.get_command('show version')
-
-        self.facts['version'] = self.parse_version(data)
-        self.facts['serialnum'] = self.parse_serialnum(data)
-        self.facts['model'] = self.parse_model(data)
-        self.facts['image'] = self.parse_image(data)
-        self.facts['hostname'] = self.parse_hostname(data)
+        data = self.run('show version')
+        if data:
+            self.facts['version'] = self.parse_version(data)
+            self.facts['serialnum'] = self.parse_serialnum(data)
+            self.facts['model'] = self.parse_model(data)
+            self.facts['image'] = self.parse_image(data)
+            self.facts['hostname'] = self.parse_hostname(data)
 
     def parse_version(self, data):
         match = re.search(r'Version (\S+),', data)
@@ -194,20 +197,17 @@ def parse_serialnum(self, data):
 
 class Hardware(FactsBase):
 
-    def commands(self):
-        add_command(self.runner, 'dir | include Directory')
-        add_command(self.runner, 'show version')
-        add_command(self.runner, 'show memory statistics | include Processor')
-
     def populate(self):
-        data = self.runner.get_command('dir | include Directory')
-        self.facts['filesystems'] = self.parse_filesystems(data)
+        data = self.run('dir | include Directory')
+        if data:
+            self.facts['filesystems'] = self.parse_filesystems(data)
 
-        data = self.runner.get_command('show memory statistics | include Processor')
-        match = re.findall(r'\s(\d+)\s', data)
-        if match:
-            self.facts['memtotal_mb'] = int(match[0]) / 1024
-            self.facts['memfree_mb'] = int(match[1]) / 1024
+        data = self.run('show memory statistics | include Processor')
+        if data:
+            match = re.findall(r'\s(\d+)\s', data)
+            if match:
+                self.facts['memtotal_mb'] = int(match[0]) / 1024
+                self.facts['memfree_mb'] = int(match[1]) / 1024
 
     def parse_filesystems(self, data):
         return re.findall(r'^Directory of (\S+)/', data, re.M)
@@ -215,37 +215,33 @@ def parse_filesystems(self, data):
 
 class Config(FactsBase):
 
-    def commands(self):
-        add_command(self.runner, 'show running-config')
-
     def populate(self):
-        self.facts['config'] = self.runner.get_command('show running-config')
+        data = self.run('show running-config')
+        if data:
+            self.facts['config'] = data
 
 
 class Interfaces(FactsBase):
 
-    def commands(self):
-        add_command(self.runner, 'show interfaces')
-        add_command(self.runner, 'show ipv6 interface')
-        add_command(self.runner, 'show lldp')
-        add_command(self.runner, 'show lldp neighbors detail')
-
     def populate(self):
         self.facts['all_ipv4_addresses'] = list()
         self.facts['all_ipv6_addresses'] = list()
 
-        data = self.runner.get_command('show interfaces')
-        interfaces = self.parse_interfaces(data)
-        self.facts['interfaces'] = self.populate_interfaces(interfaces)
+        data = self.run('show interfaces')
+        if data:
+            interfaces = self.parse_interfaces(data)
+            self.facts['interfaces'] = self.populate_interfaces(interfaces)
 
-        data = self.runner.get_command('show ipv6 interface')
-        if len(data) > 0:
+        data = self.run('show ipv6 interface')
+        if data:
             data = self.parse_interfaces(data)
             self.populate_ipv6_interfaces(data)
 
-        if 'LLDP is not enabled' not in self.runner.get_command('show lldp'):
-            neighbors = self.runner.get_command('show lldp neighbors detail')
-            self.facts['neighbors'] = self.parse_neighbors(neighbors)
+        data = self.run('show lldp')
+        if 'LLDP is not enabled' not in data:
+            neighbors = self.run('show lldp neighbors detail')
+            if neighbors:
+                self.facts['neighbors'] = self.parse_neighbors(neighbors)
 
     def populate_interfaces(self, interfaces):
         facts = dict()
@@ -434,27 +430,27 @@ def main():
     facts = dict()
     facts['gather_subset'] = list(runable_subsets)
 
-    runner = CommandRunner(module)
-
     instances = list()
     for key in runable_subsets:
-        instances.append(FACT_SUBSETS[key](runner))
+        instances.append(FACT_SUBSETS[key](module))
 
-    runner.run()
+    failed_commands = list()
 
     try:
         for inst in instances:
             inst.populate()
+            failed_commands.extend(inst.failed_commands)
             facts.update(inst.facts)
     except Exception:
-        module.exit_json(out=module.from_json(runner.items))
+        exc = get_exception()
+        module.fail_json(msg=str(exc))
 
     ansible_facts = dict()
     for key, value in iteritems(facts):
         key = 'ansible_net_%s' % key
         ansible_facts[key] = value
 
-    module.exit_json(ansible_facts=ansible_facts)
+    module.exit_json(ansible_facts=ansible_facts, failed_commands=failed_commands)
 
 
 if __name__ == '__main__':


Test patch: