diff --git a/lib/ansible/modules/network/cli/__init__.py b/lib/ansible/modules/network/cli/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/ansible/modules/network/cli/cli_command.py b/lib/ansible/modules/network/cli/cli_command.py
new file mode 100644
index 00000000000..ec5fe1c08ea
--- /dev/null
+++ b/lib/ansible/modules/network/cli/cli_command.py
@@ -0,0 +1,230 @@
+#!/usr/bin/python
+# Copyright: Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'network'}
+
+
+DOCUMENTATION = """
+---
+module: cli_command
+version_added: "2.7"
+author: "Nathaniel Case (@qalthos)"
+short_description: Run arbitrary commands on cli-based network devices
+description:
+  - Sends an arbitrary set of commands to a network device and returns the
+    results read from the device.  This module includes an argument that
+    will cause the module to wait for a specific condition before returning
+    or timing out if the condition is not met.
+notes:
+  - Tested against EOS 4.15
+options:
+  commands:
+    description:
+      - The commands to send to the remote EOS device over the
+        configured provider.  The resulting output from the command
+        is returned.  If the I(wait_for) argument is provided, the
+        module is not returned until the condition is satisfied or
+        the number of I(retries) has been exceeded.
+    required: true
+  wait_for:
+    description:
+      - Specifies what to evaluate from the output of the command
+        and what conditionals to apply.  This argument will cause
+        the task to wait for a particular conditional to be true
+        before moving forward.   If the conditional is not true
+        by the configured retries, the task fails.
+        Note - With I(wait_for) the value in C(result['stdout']) can be accessed
+        using C(result), that is to access C(result['stdout'][0]) use C(result[0]) See examples.
+    aliases: ['waitfor']
+    version_added: "2.2"
+  match:
+    description:
+      - The I(match) argument is used in conjunction with the
+        I(wait_for) argument to specify the match policy.  Valid
+        values are C(all) or C(any).  If the value is set to C(all)
+        then all conditionals in the I(wait_for) must be satisfied.  If
+        the value is set to C(any) then only one of the values must be
+        satisfied.
+    default: all
+    choices: ['any', 'all']
+    version_added: "2.2"
+  retries:
+    description:
+      - Specifies the number of retries a command should be tried
+        before it is considered failed.  The command is run on the
+        target device every retry and evaluated against the I(wait_for)
+        conditionals.
+    default: 10
+  interval:
+    description:
+      - Configures the interval in seconds to wait between retries
+        of the command.  If the command does not pass the specified
+        conditional, the interval indicates how to long to wait before
+        trying the command again.
+    default: 1
+"""
+
+EXAMPLES = """
+- name: run show version on remote devices
+  cli_command:
+    commands: show version
+
+- name: run show version and check to see if output contains Arista
+  cli_command:
+    commands: show version
+    wait_for: result[0] contains Arista
+
+- name: run multiple commands on remote nodes
+  cli_command:
+    commands:
+      - show version
+      - show interfaces
+
+- name: run multiple commands and evaluate the output
+  cli_command:
+    commands:
+      - show version
+      - show interfaces
+    wait_for:
+      - result[0] contains Arista
+      - result[1] contains Loopback0
+
+- name: run commands and specify the output format
+  cli_command:
+    commands:
+      - command: show version
+        output: json
+"""
+
+RETURN = """
+stdout:
+  description: The set of responses from the commands
+  returned: always apart from low level errors (such as action plugin)
+  type: list
+  sample: ['...', '...']
+stdout_lines:
+  description: The value of stdout split into a list
+  returned: always apart from low level errors (such as action plugin)
+  type: list
+  sample: [['...', '...'], ['...'], ['...']]
+failed_conditions:
+  description: The list of conditionals that have failed
+  returned: failed
+  type: list
+  sample: ['...', '...']
+"""
+import time
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import string_types
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.connection import Connection, ConnectionError
+from ansible.module_utils.network.common.parsing import Conditional
+from ansible.module_utils.network.common.utils import ComplexList
+
+VALID_KEYS = ['command', 'output', 'prompt', 'response']
+
+
+def to_lines(output):
+    lines = []
+    for item in output:
+        if isinstance(item, string_types):
+            item = to_text(item).split('\n')
+        lines.append(item)
+    return lines
+
+
+def parse_commands(module, warnings):
+    transform = ComplexList(dict(
+        command=dict(key=True),
+        output=dict(),
+        prompt=dict(),
+        answer=dict()
+    ), module)
+
+    commands = transform(module.params['commands'])
+
+    if module.check_mode:
+        for item in list(commands):
+            if not item['command'].startswith('show'):
+                warnings.append(
+                    'Only show commands are supported when using check_mode, not '
+                    'executing %s' % item['command']
+                )
+                commands.remove(item)
+
+    return commands
+
+
+def main():
+    """entry point for module execution
+    """
+    argument_spec = dict(
+        commands=dict(type='list', required=True),
+
+        wait_for=dict(type='list', aliases=['waitfor']),
+        match=dict(default='all', choices=['all', 'any']),
+
+        retries=dict(default=10, type='int'),
+        interval=dict(default=1, type='int')
+    )
+    module = AnsibleModule(argument_spec=argument_spec,
+                           supports_check_mode=True)
+
+    warnings = list()
+    result = {'changed': False, 'warnings': warnings}
+
+    wait_for = module.params['wait_for'] or list()
+    try:
+        conditionals = [Conditional(c) for c in wait_for]
+    except AttributeError as exc:
+        module.fail_json(msg=to_text(exc))
+
+    commands = parse_commands(module, warnings)
+    retries = module.params['retries']
+    interval = module.params['interval']
+    match = module.params['match']
+
+    connection = Connection(module._socket_path)
+    for attempt in range(retries):
+        responses = []
+        try:
+            for command in commands:
+                responses.append(connection.get(**command))
+        except ConnectionError as exc:
+            module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
+
+        for item in list(conditionals):
+            if item(responses):
+                if match == 'any':
+                    conditionals = list()
+                    break
+                conditionals.remove(item)
+
+        if not conditionals:
+            break
+
+        time.sleep(interval)
+
+    if conditionals:
+        failed_conditions = [item.raw for item in conditionals]
+        msg = 'One or more conditional statements have not been satisfied'
+        module.fail_json(msg=msg, failed_conditions=failed_conditions)
+
+    result.update({
+        'stdout': responses,
+        'stdout_lines': to_lines(responses)
+    })
+
+    module.exit_json(**result)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/lib/ansible/plugins/action/cli_command.py b/lib/ansible/plugins/action/cli_command.py
new file mode 100644
index 00000000000..ff191f56212
--- /dev/null
+++ b/lib/ansible/plugins/action/cli_command.py
@@ -0,0 +1,31 @@
+#
+# Copyright 2018 Red Hat Inc.
+#
+# 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/>.
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.action.normal import ActionModule as _ActionModule
+
+
+class ActionModule(_ActionModule):
+
+    def run(self, tmp=None, task_vars=None):
+        if self._play_context.connection != 'network_cli':
+            return {'failed': True, 'msg': 'Connection type %s is not valid for this module' % self._play_context.connection}
+
+        return super(ActionModule, self).run(task_vars=task_vars)
diff --git a/lib/ansible/plugins/cliconf/edgeos.py b/lib/ansible/plugins/cliconf/edgeos.py
index c623d925508..0057fb2fee3 100644
--- a/lib/ansible/plugins/cliconf/edgeos.py
+++ b/lib/ansible/plugins/cliconf/edgeos.py
@@ -41,11 +41,11 @@ class Cliconf(CliconfBase):
     def get_config(self, source='running', format='text'):
         return self.send_command('show configuration commands')
 
-    def edit_config(self, command):
-        for cmd in chain(['configure'], to_list(command)):
+    def edit_config(self, candidate=None, commit=True, replace=False, comment=None):
+        for cmd in chain(['configure'], to_list(candidate)):
             self.send_command(cmd)
 
-    def get(self, command, prompt=None, answer=None, sendonly=False):
+    def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None):
         return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
 
     def commit(self, comment=None):
diff --git a/lib/ansible/plugins/cliconf/iosxr.py b/lib/ansible/plugins/cliconf/iosxr.py
index 61d1f899e44..ac06528c6e9 100644
--- a/lib/ansible/plugins/cliconf/iosxr.py
+++ b/lib/ansible/plugins/cliconf/iosxr.py
@@ -86,7 +86,7 @@ class Cliconf(CliconfBase):
 
             self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
 
-    def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True):
+    def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, output=None):
         return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
 
     def commit(self, comment=None, label=None):
diff --git a/test/integration/targets/eos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/eos_smoke/tests/cli/cli_command.yaml
new file mode 100644
index 00000000000..d8d42882b68
--- /dev/null
+++ b/test/integration/targets/eos_smoke/tests/cli/cli_command.yaml
@@ -0,0 +1,42 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- name: command that does require become (should fail)
+  cli_command:
+    commands:
+      - show running-config
+  become: no
+  ignore_errors: yes
+  register: result
+
+- assert:
+    that:
+      - 'result.failed == true'
+      - '"privileged mode required" in result.msg'
+
+- name: get output for single command
+  cli_command:
+    commands:
+      - show version
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+
+- name: get output for multiple commands
+  cli_command:
+    commands:
+      - show version
+      - show interfaces
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+      - "result.stdout | length == 2"
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/eos_smoke/tests/cli/misc_tests.yaml b/test/integration/targets/eos_smoke/tests/cli/misc_tests.yaml
index c38e194df6f..a11703589dc 100644
--- a/test/integration/targets/eos_smoke/tests/cli/misc_tests.yaml
+++ b/test/integration/targets/eos_smoke/tests/cli/misc_tests.yaml
@@ -15,7 +15,7 @@
     - assert:
         that:
           - 'result.failed == true'
-          - '"privileged mode required" in result.module_stderr'
+          - '"privileged mode required" in result.msg'
 
     - name: command that doesn't require become
       eos_command:
diff --git a/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml b/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml
new file mode 100644
index 00000000000..e31c151229a
--- /dev/null
+++ b/test/integration/targets/ios_smoke/tests/cli/cli_command.yaml
@@ -0,0 +1,32 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- block:
+    - name: get output for single command
+      cli_command:
+        commands:
+          - show version
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+
+    - name: get output for multiple commands
+      cli_command:
+        commands:
+          - show version
+          - show interfaces
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+          - "result.stdout | length == 2"
+
+  when: ansible_connection == 'network_cli'
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/ios_smoke/tests/cli/misc_tests.yaml b/test/integration/targets/ios_smoke/tests/cli/misc_tests.yaml
index e9a116ed0f6..a5918193322 100644
--- a/test/integration/targets/ios_smoke/tests/cli/misc_tests.yaml
+++ b/test/integration/targets/ios_smoke/tests/cli/misc_tests.yaml
@@ -34,7 +34,7 @@
 - assert:
     that:
       - 'result.failed == true'
-      - "'timeout trying to send command' in result.module_stderr"
+      - "'timeout trying to send command' in result.msg"
   when: ansible_connection == 'network_cli'
 
 - debug: msg="END ios_smoke cli/misc_tests.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml b/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml
new file mode 100644
index 00000000000..e31c151229a
--- /dev/null
+++ b/test/integration/targets/iosxr_smoke/tests/cli/cli_command.yaml
@@ -0,0 +1,32 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- block:
+    - name: get output for single command
+      cli_command:
+        commands:
+          - show version
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+
+    - name: get output for multiple commands
+      cli_command:
+        commands:
+          - show version
+          - show interfaces
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+          - "result.stdout | length == 2"
+
+  when: ansible_connection == 'network_cli'
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/junos_smoke/tasks/cli.yaml b/test/integration/targets/junos_smoke/tasks/cli.yaml
new file mode 100644
index 00000000000..3f93a4f3696
--- /dev/null
+++ b/test/integration/targets/junos_smoke/tasks/cli.yaml
@@ -0,0 +1,16 @@
+---
+- name: collect all cli test cases
+  find:
+    paths: "{{ role_path }}/tests/cli"
+    patterns: "{{ testcase }}.yaml"
+  register: test_cases
+  delegate_to: localhost
+
+- name: set test_items
+  set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: run test case (connection=network_cli)
+  include: "{{ test_case_to_run }} ansible_connection=network_cli"
+  with_items: "{{ test_items }}"
+  loop_control:
+    loop_var: test_case_to_run
diff --git a/test/integration/targets/junos_smoke/tasks/main.yaml b/test/integration/targets/junos_smoke/tasks/main.yaml
index cc27f174fd8..f0d6ea992f6 100644
--- a/test/integration/targets/junos_smoke/tasks/main.yaml
+++ b/test/integration/targets/junos_smoke/tasks/main.yaml
@@ -1,2 +1,3 @@
 ---
 - { include: netconf.yaml, tags: ['netconf'] }
+- { include: cli.yaml, tags: ['cli'] }
diff --git a/test/integration/targets/junos_smoke/tests/cli/cli_commmand.yaml b/test/integration/targets/junos_smoke/tests/cli/cli_commmand.yaml
new file mode 100644
index 00000000000..b23b81c4627
--- /dev/null
+++ b/test/integration/targets/junos_smoke/tests/cli/cli_commmand.yaml
@@ -0,0 +1,29 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- name: get output for single command
+  cli_command:
+    commands:
+      - show version
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+
+- name: get output for multiple commands
+  cli_command:
+    commands:
+      - show version
+      - show interfaces
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+      - "result.stdout | length == 2"
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml
new file mode 100644
index 00000000000..f9c10fad877
--- /dev/null
+++ b/test/integration/targets/nxos_smoke/tests/cli/cli_command.yaml
@@ -0,0 +1,32 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- block:
+    - name: get output for single command
+      cli_command:
+        commands:
+          - show version
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+
+    - name: get output for multiple commands
+      cli_command:
+        commands:
+          - show version
+          - show interface
+      register: result
+
+    - assert:
+        that:
+          - "result.changed == false"
+          - "result.stdout is defined"
+          - "result.stdout | length == 2"
+
+  when: ansible_connection == 'network_cli'
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml b/test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml
new file mode 100644
index 00000000000..b23b81c4627
--- /dev/null
+++ b/test/integration/targets/vyos_smoke/tests/cli/cli_command.yaml
@@ -0,0 +1,29 @@
+---
+- debug:
+    msg: "START cli/cli_command.yaml on connection={{ ansible_connection }}"
+
+- name: get output for single command
+  cli_command:
+    commands:
+      - show version
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+
+- name: get output for multiple commands
+  cli_command:
+    commands:
+      - show version
+      - show interfaces
+  register: result
+
+- assert:
+    that:
+      - "result.changed == false"
+      - "result.stdout is defined"
+      - "result.stdout | length == 2"
+
+- debug: msg="END cli/cli_command.yaml on connection={{ ansible_connection }}"