mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-27 05:58:50 +00:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
0
plugins/cliconf/__init__.py
Normal file
0
plugins/cliconf/__init__.py
Normal file
95
plugins/cliconf/aireos.py
Normal file
95
plugins/cliconf/aireos.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: aireos
|
||||
short_description: Use aireos cliconf to run command on Cisco WLC platform
|
||||
description:
|
||||
- This aireos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Cisco WLC network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'aireos'
|
||||
reply = self.get('show sysinfo')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Product Version\.* (.*)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'System Name\.* (.*)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
reply = self.get('show inventory')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'DESCR: \"(.*)\"', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
if source == 'running':
|
||||
cmd = 'show run-config commands'
|
||||
else:
|
||||
cmd = 'show run-config startup-commands'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['config'], to_list(command), ['end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
self._update_cli_prompt_context(config_context=')#')
|
||||
72
plugins/cliconf/apconos.py
Normal file
72
plugins/cliconf/apconos.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# (C) 2018 Red Hat Inc.
|
||||
# Copyright (C) 2019 APCON.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
# Contains CLIConf Plugin methods for apconos Modules
|
||||
# APCON Networking
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author: "David Li (@davidlee-ap)"
|
||||
cliconf: apconos
|
||||
short_description: Use apconos cliconf to run command on APCON network devices
|
||||
description:
|
||||
- This apconos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from APCON network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'apconos'
|
||||
reply = self.get(b'show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
if data:
|
||||
device_info['network_os_version'] = self.parse_version(data)
|
||||
device_info['network_os_model'] = self.parse_model(data)
|
||||
|
||||
return device_info
|
||||
|
||||
def parse_version(self, data):
|
||||
return ""
|
||||
|
||||
def parse_model(self, data):
|
||||
return ""
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text'):
|
||||
pass
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure terminal'], to_list(command), [b'end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
return json.dumps(self.get_device_info())
|
||||
95
plugins/cliconf/aruba.py
Normal file
95
plugins/cliconf/aruba.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: aruba
|
||||
short_description: Use aruba cliconf to run command on Aruba platform
|
||||
description:
|
||||
- This aruba plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Aruba network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'aruba'
|
||||
reply = self.get('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'^MODEL: (\S+)\),', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
reply = self.get('show hostname')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'^Hostname is (.+)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
if source == 'running':
|
||||
cmd = 'show running-config all'
|
||||
else:
|
||||
cmd = 'show configuration'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
self._update_cli_prompt_context(config_context=')#')
|
||||
121
plugins/cliconf/ce.py
Normal file
121
plugins/cliconf/ce.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: ce
|
||||
short_description: Use ce cliconf to run command on HUAWEI CloudEngine platform
|
||||
description:
|
||||
- This ce plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from HUAWEI CloudEngine network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'ce'
|
||||
reply = self.get('display version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'^Huawei.+\n.+\Version\s+(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1).strip(',')
|
||||
|
||||
match = re.search(r'^Huawei(.+)\n.+\(\S+\s+\S+\)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'HUAWEI\s+(\S+)\s+uptime', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if not flags:
|
||||
flags = []
|
||||
|
||||
cmd = 'display current-configuration'
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
results = []
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
if isinstance(cmd, dict):
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
newline = cmd.get('newline', True)
|
||||
else:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
newline = True
|
||||
|
||||
results.append(self.send_command(command, prompt, answer, False, newline))
|
||||
return results[1:-1]
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
out = self._connection.get_prompt()
|
||||
|
||||
if out is None:
|
||||
raise AnsibleConnectionFailure(message=u'cli prompt is not identified from the last received'
|
||||
u' response window: %s' % self._connection._last_recv_window)
|
||||
|
||||
prompt = to_text(out, errors='surrogate_then_replace').strip()
|
||||
while prompt.endswith(']'):
|
||||
self._connection.queue_message('vvvv', 'wrong context, sending return to device')
|
||||
if prompt.startswith('[*'):
|
||||
self._connection.exec_command('clear configuration candidate')
|
||||
self._connection.exec_command('return')
|
||||
out = self._connection.get_prompt()
|
||||
prompt = to_text(out, errors='surrogate_then_replace').strip()
|
||||
135
plugins/cliconf/cnos.py
Normal file
135
plugins/cliconf/cnos.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# (C) 2017 Red Hat Inc.
|
||||
# Copyright (C) 2017 Lenovo.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
# Contains CLIConf Plugin methods for CNOS Modules
|
||||
# Lenovo Networking
|
||||
#
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: cnos
|
||||
short_description: Use cnos cliconf to run command on Lenovo CNOS platform
|
||||
description:
|
||||
- This cnos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Lenovo CNOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'cnos'
|
||||
reply = self.get('show sys-info')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
host = self.get('show hostname')
|
||||
hostname = to_text(host, errors='surrogate_or_strict').strip()
|
||||
if data:
|
||||
device_info['network_os_version'] = self.parse_version(data)
|
||||
device_info['network_os_model'] = self.parse_model(data)
|
||||
device_info['network_os_hostname'] = hostname
|
||||
|
||||
return device_info
|
||||
|
||||
def parse_version(self, data):
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'System Software Revision (.*?)',
|
||||
line, re.M | re.I)
|
||||
if match:
|
||||
vers = line.split(':')
|
||||
ver = vers[1].strip()
|
||||
return ver
|
||||
return "NA"
|
||||
|
||||
def parse_model(self, data):
|
||||
for line in data.split('\n'):
|
||||
line = line.strip()
|
||||
match = re.match(r'System Model (.*?)', line, re.M | re.I)
|
||||
if match:
|
||||
mdls = line.split(':')
|
||||
mdl = mdls[1].strip()
|
||||
return mdl
|
||||
return "NA"
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
msg = "fetching configuration from %s is not supported"
|
||||
return self.invalid_params(msg % source)
|
||||
if source == 'running':
|
||||
cmd = 'show running-config'
|
||||
else:
|
||||
cmd = 'show startup-config'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, candidate=None, commit=True,
|
||||
replace=None, comment=None):
|
||||
resp = {}
|
||||
results = []
|
||||
requests = []
|
||||
if commit:
|
||||
self.send_command('configure terminal')
|
||||
for line in to_list(candidate):
|
||||
if not isinstance(line, Mapping):
|
||||
line = {'command': line}
|
||||
|
||||
cmd = line['command']
|
||||
if cmd != 'end' and cmd[0] != '!':
|
||||
results.append(self.send_command(**line))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end')
|
||||
else:
|
||||
raise ValueError('check mode is not supported')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
out = self._connection.get_prompt()
|
||||
|
||||
if out is None:
|
||||
raise AnsibleConnectionFailure(message=u'cli prompt is not identified from the last received'
|
||||
u' response window: %s' % self._connection._last_recv_window)
|
||||
|
||||
if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'):
|
||||
self._connection.queue_message('vvvv', 'In Config mode, sending exit to device')
|
||||
self._connection.send_command('exit')
|
||||
else:
|
||||
self._connection.send_command('enable')
|
||||
114
plugins/cliconf/edgeos.py
Normal file
114
plugins/cliconf/edgeos.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Copyright: (c) 2018, 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: edgeos
|
||||
short_description: Use edgeos cliconf to run command on EdgeOS platform
|
||||
description:
|
||||
- This edgeos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Ubiquiti EdgeOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'edgeos'
|
||||
reply = self.get('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version:\s*v?(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'HW model:\s*(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
reply = self.get('show host name')
|
||||
device_info['network_os_hostname'] = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
return self.send_command('show configuration commands')
|
||||
|
||||
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, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def commit(self, comment=None):
|
||||
if comment:
|
||||
command = 'commit comment "{0}"'.format(comment)
|
||||
else:
|
||||
command = 'commit'
|
||||
self.send_command(command)
|
||||
|
||||
def discard_changes(self, *args, **kwargs):
|
||||
self.send_command('discard')
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for run_commands" % output)
|
||||
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc:
|
||||
raise
|
||||
out = getattr(e, 'err', e)
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
|
||||
def get_device_operations(self):
|
||||
return {
|
||||
'supports_diff_replace': False,
|
||||
'supports_commit': True,
|
||||
'supports_rollback': False,
|
||||
'supports_defaults': False,
|
||||
'supports_onbox_diff': False,
|
||||
'supports_commit_comment': True,
|
||||
'supports_multiline_delimiter': False,
|
||||
'supports_diff_match': False,
|
||||
'supports_diff_ignore_lines': False,
|
||||
'supports_generate_diff': False,
|
||||
'supports_replace': False
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
result['rpc'] += ['commit', 'discard_changes', 'run_commands']
|
||||
result['device_operations'] = self.get_device_operations()
|
||||
return json.dumps(result)
|
||||
141
plugins/cliconf/edgeswitch.py
Normal file
141
plugins/cliconf/edgeswitch.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
#
|
||||
# (c) 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: edgeswitch
|
||||
short_description: Use edgeswitch cliconf to run command on EdgeSwitch platform
|
||||
description:
|
||||
- This edgeswitch plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Ubiquiti EdgeSwitch network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import dumps
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'edgeswitch'
|
||||
reply = self.get(command='show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Software Version\.+ (.*)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1).strip(',')
|
||||
|
||||
match = re.search(r'^Machine Model\.+ (.*)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'System Name\.+ (.*)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if source == 'running':
|
||||
cmd = 'show running-config '
|
||||
else:
|
||||
cmd = 'show startup-config '
|
||||
|
||||
if flags:
|
||||
cmd += ' '.join(to_list(flags))
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, commands):
|
||||
resp = {}
|
||||
|
||||
results = []
|
||||
requests = []
|
||||
self.send_command('configure')
|
||||
for line in to_list(commands):
|
||||
if not isinstance(line, Mapping):
|
||||
line = {'command': line}
|
||||
|
||||
cmd = line['command']
|
||||
if cmd != 'end' and cmd[0] != '!':
|
||||
results.append(self.send_command(**line))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, newline=True, check_all=False):
|
||||
if not command:
|
||||
raise ValueError('must provide value of command to execute')
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for get" % output)
|
||||
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
result['rpc'] += ['run_commands']
|
||||
return json.dumps(result)
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for run_commands" % output)
|
||||
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc:
|
||||
raise
|
||||
out = getattr(e, 'err', e)
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
103
plugins/cliconf/enos.py
Normal file
103
plugins/cliconf/enos.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# (C) 2017 Red Hat Inc.
|
||||
# Copyright (C) 2017 Lenovo.
|
||||
#
|
||||
# GNU General Public License v3.0+
|
||||
#
|
||||
# This program 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.
|
||||
#
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
#
|
||||
# Contains CLIConf Plugin methods for ENOS Modules
|
||||
# Lenovo Networking
|
||||
#
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: enos
|
||||
short_description: Use enos cliconf to run command on Lenovo ENOS platform
|
||||
description:
|
||||
- This enos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Lenovo ENOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'enos'
|
||||
reply = self.get('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'^Software Version (.*?) ', data, re.M | re.I)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'^Lenovo RackSwitch (\S+)', data, re.M | re.I)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'^(.+) uptime', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
else:
|
||||
device_info['network_os_hostname'] = "NA"
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
msg = "fetching configuration from %s is not supported"
|
||||
return self.invalid_params(msg % source)
|
||||
if source == 'running':
|
||||
cmd = 'show running-config'
|
||||
else:
|
||||
cmd = 'show startup-config'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
out = self._connection.get_prompt()
|
||||
|
||||
if out is None:
|
||||
raise AnsibleConnectionFailure(message=u'cli prompt is not identified from the last received'
|
||||
u' response window: %s' % self._connection._last_recv_window)
|
||||
|
||||
if to_text(out, errors='surrogate_then_replace').strip().endswith(')#'):
|
||||
self._connection.queue_message('vvvv', 'In Config mode, sending exit to device')
|
||||
self._connection.send_command('exit')
|
||||
else:
|
||||
self._connection.send_command('enable')
|
||||
97
plugins/cliconf/eric_eccli.py
Normal file
97
plugins/cliconf/eric_eccli.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#
|
||||
# Copyright (c) 2019 Ericsson AB.
|
||||
#
|
||||
# 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author: Ericsson IPOS OAM team
|
||||
cliconf: eccli
|
||||
short_description: Use eccli cliconf to run command on Ericsson ECCLI platform
|
||||
description:
|
||||
- This eccli plugin provides low level abstraction APIs for
|
||||
sending and receiving CLI commands from Ericsson ECCLI network devices.
|
||||
'''
|
||||
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
import collections
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_config(self, source='running', flags=None, format=None):
|
||||
return
|
||||
|
||||
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
|
||||
return
|
||||
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, newline=True, check_all=False):
|
||||
if not command:
|
||||
raise ValueError('must provide value of command to execute')
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for get" % output)
|
||||
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
device_info['network_os'] = 'eric_eccli'
|
||||
return device_info
|
||||
|
||||
def get_capabilities(self):
|
||||
result = dict()
|
||||
result['rpc'] = self.get_base_rpc() + ['run_commands']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for run_commands" % output)
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc:
|
||||
raise
|
||||
out = getattr(e, 'err', e)
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
229
plugins/cliconf/exos.py
Normal file
229
plugins/cliconf/exos.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: exos
|
||||
short_description: Use exos cliconf to run command on Extreme EXOS platform
|
||||
description:
|
||||
- This exos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Extreme EXOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.module_utils.connection import ConnectionError
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
diff = {}
|
||||
device_operations = self.get_device_operations()
|
||||
option_values = self.get_option_values()
|
||||
|
||||
if candidate is None and device_operations['supports_generate_diff']:
|
||||
raise ValueError("candidate configuration is required to generate diff")
|
||||
|
||||
if diff_match not in option_values['diff_match']:
|
||||
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
|
||||
|
||||
if diff_replace not in option_values['diff_replace']:
|
||||
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = NetworkConfig(indent=1)
|
||||
candidate_obj.load(candidate)
|
||||
|
||||
if running and diff_match != 'none' and diff_replace != 'config':
|
||||
# running configuration
|
||||
running_obj = NetworkConfig(indent=1, contents=running, ignore_lines=diff_ignore_lines)
|
||||
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
|
||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||
return diff
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
device_info['network_os'] = 'exos'
|
||||
|
||||
reply = self.run_commands({'command': 'show switch detail', 'output': 'text'})
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'ExtremeXOS version (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'System Type: +(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'SysName: +(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_default_flag(self):
|
||||
# The flag to modify the command to collect configuration with defaults
|
||||
return 'detail'
|
||||
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
options_values = self.get_option_values()
|
||||
if format not in options_values['format']:
|
||||
raise ValueError("'format' value %s is invalid. Valid values are %s" % (format, ','.join(options_values['format'])))
|
||||
|
||||
lookup = {'running': 'show configuration', 'startup': 'debug cfgmgr show configuration file'}
|
||||
if source not in lookup:
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
|
||||
cmd = {'command': lookup[source], 'output': 'text'}
|
||||
|
||||
if source == 'startup':
|
||||
reply = self.run_commands({'command': 'show switch', 'format': 'text'})
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
match = re.search(r'Config Selected: +(\S+)\.cfg', data, re.MULTILINE)
|
||||
if match:
|
||||
cmd['command'] += match.group(1)
|
||||
else:
|
||||
# No Startup(/Selected) Config
|
||||
return {}
|
||||
|
||||
cmd['command'] += ' '.join(to_list(flags))
|
||||
cmd['command'] = cmd['command'].strip()
|
||||
|
||||
return self.run_commands(cmd)[0]
|
||||
|
||||
def edit_config(self, candidate=None, commit=True, replace=None, diff=False, comment=None):
|
||||
resp = {}
|
||||
operations = self.get_device_operations()
|
||||
self.check_edit_config_capability(operations, candidate, commit, replace, comment)
|
||||
results = []
|
||||
requests = []
|
||||
|
||||
if commit:
|
||||
for line in to_list(candidate):
|
||||
if not isinstance(line, Mapping):
|
||||
line = {'command': line}
|
||||
results.append(self.send_command(**line))
|
||||
requests.append(line['command'])
|
||||
else:
|
||||
raise ValueError('check mode is not supported')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, output=None, newline=True, check_all=False):
|
||||
if output:
|
||||
command = self._get_command_with_output(command, output)
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
cmd['command'] = self._get_command_with_output(cmd['command'], output)
|
||||
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc is True:
|
||||
raise
|
||||
out = getattr(e, 'err', e)
|
||||
|
||||
if out is not None:
|
||||
try:
|
||||
out = to_text(out, errors='surrogate_or_strict').strip()
|
||||
except UnicodeError:
|
||||
raise ConnectionError(message=u'Failed to decode output from %s: %s' % (cmd, to_text(out)))
|
||||
|
||||
if output and output == 'json':
|
||||
try:
|
||||
out = json.loads(out)
|
||||
except ValueError:
|
||||
raise ConnectionError('Response was not valid JSON, got {0}'.format(
|
||||
to_text(out)
|
||||
))
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
|
||||
def get_device_operations(self):
|
||||
return {
|
||||
'supports_diff_replace': False, # identify if config should be merged or replaced is supported
|
||||
'supports_commit': False, # identify if commit is supported by device or not
|
||||
'supports_rollback': False, # identify if rollback is supported or not
|
||||
'supports_defaults': True, # identify if fetching running config with default is supported
|
||||
'supports_commit_comment': False, # identify if adding comment to commit is supported of not
|
||||
'supports_onbox_diff': False, # identify if on box diff capability is supported or not
|
||||
'supports_generate_diff': True, # identify if diff capability is supported within plugin
|
||||
'supports_multiline_delimiter': False, # identify if multiline delimiter is supported within config
|
||||
'supports_diff_match': True, # identify if match is supported
|
||||
'supports_diff_ignore_lines': True, # identify if ignore line in diff is supported
|
||||
'supports_config_replace': False, # identify if running config replace with candidate config is supported
|
||||
'supports_admin': False, # identify if admin configure mode is supported or not
|
||||
'supports_commit_label': False, # identify if commit label is supported or not
|
||||
'supports_replace': False
|
||||
}
|
||||
|
||||
def get_option_values(self):
|
||||
return {
|
||||
'format': ['text', 'json'],
|
||||
'diff_match': ['line', 'strict', 'exact', 'none'],
|
||||
'diff_replace': ['line', 'block'],
|
||||
'output': ['text', 'json']
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
result['rpc'] += ['run_commmands', 'get_default_flag', 'get_diff']
|
||||
result['device_operations'] = self.get_device_operations()
|
||||
result['device_info'] = self.get_device_info()
|
||||
result.update(self.get_option_values())
|
||||
return json.dumps(result)
|
||||
|
||||
def _get_command_with_output(self, command, output):
|
||||
if output not in self.get_option_values().get('output'):
|
||||
raise ValueError("'output' value is %s is invalid. Valid values are %s" % (output, ','.join(self.get_option_values().get('output'))))
|
||||
|
||||
if output == 'json' and not command.startswith('run script cli2json.py'):
|
||||
cmd = 'run script cli2json.py %s' % command
|
||||
else:
|
||||
cmd = command
|
||||
return cmd
|
||||
314
plugins/cliconf/icx.py
Normal file
314
plugins/cliconf/icx.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# Copyright: (c) 2019, 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author: Ruckus Wireless (@Commscope)
|
||||
cliconf: icx
|
||||
short_description: Use icx cliconf to run command on Ruckus ICX platform
|
||||
description:
|
||||
- This icx plugin provides low level abstraction APIs for
|
||||
sending and receiving CLI commands from Ruckus ICX network devices.
|
||||
'''
|
||||
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
|
||||
from itertools import chain
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', flags=None, format=None, compare=None):
|
||||
if source not in ('running', 'startup'):
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if format:
|
||||
raise ValueError("'format' value %s is not supported for get_config" % format)
|
||||
|
||||
if not flags:
|
||||
flags = []
|
||||
|
||||
if compare is False:
|
||||
return ''
|
||||
else:
|
||||
if source == 'running':
|
||||
cmd = 'show running-config '
|
||||
else:
|
||||
cmd = 'show configuration '
|
||||
|
||||
cmd += ' '.join(to_list(flags))
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
"""
|
||||
Generate diff between candidate and running configuration. If the
|
||||
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
|
||||
candidate and running configurations are not required to be passed as argument.
|
||||
In case if onbox diff capability is not supported candidate argument is mandatory
|
||||
and running argument is optional.
|
||||
:param candidate: The configuration which is expected to be present on remote host.
|
||||
:param running: The base configuration which is used to generate diff.
|
||||
:param diff_match: Instructs how to match the candidate configuration with current device configuration
|
||||
Valid values are 'line', 'strict', 'exact', 'none'.
|
||||
'line' - commands are matched line by line
|
||||
'strict' - command lines are matched with respect to position
|
||||
'exact' - command lines must be an equal match
|
||||
'none' - will not compare the candidate configuration with the running configuration
|
||||
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
|
||||
ignored during the diff. This is used for lines in the configuration
|
||||
that are automatically updated by the system. This argument takes
|
||||
a list of regular expressions or exact line matches.
|
||||
:param path: The ordered set of parents that uniquely identify the section or hierarchy
|
||||
the commands should be checked against. If the parents argument
|
||||
is omitted, the commands are checked against the set of top
|
||||
level or global commands.
|
||||
:param diff_replace: Instructs on the way to perform the configuration on the device.
|
||||
If the replace argument is set to I(line) then the modified lines are
|
||||
pushed to the device in configuration mode. If the replace argument is
|
||||
set to I(block) then the entire command block is pushed to the device in
|
||||
configuration mode if any line is not correct.
|
||||
:return: Configuration diff in json format.
|
||||
{
|
||||
'config_diff': '',
|
||||
'banner_diff': {}
|
||||
}
|
||||
|
||||
"""
|
||||
diff = {}
|
||||
device_operations = self.get_device_operations()
|
||||
option_values = self.get_option_values()
|
||||
|
||||
if candidate is None and device_operations['supports_generate_diff']:
|
||||
raise ValueError("candidate configuration is required to generate diff")
|
||||
|
||||
if diff_match not in option_values['diff_match']:
|
||||
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
|
||||
|
||||
if diff_replace not in option_values['diff_replace']:
|
||||
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = NetworkConfig(indent=1)
|
||||
want_src, want_banners = self._extract_banners(candidate)
|
||||
candidate_obj.load(want_src)
|
||||
|
||||
if running and diff_match != 'none':
|
||||
# running configuration
|
||||
have_src, have_banners = self._extract_banners(running)
|
||||
|
||||
running_obj = NetworkConfig(indent=1, contents=have_src, ignore_lines=diff_ignore_lines)
|
||||
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
have_banners = {}
|
||||
|
||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||
|
||||
banners = self._diff_banners(want_banners, have_banners)
|
||||
diff['banner_diff'] = banners if banners else {}
|
||||
return diff
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
|
||||
resp = {}
|
||||
operations = self.get_device_operations()
|
||||
self.check_edit_config_capability(operations, candidate, commit, replace, comment)
|
||||
|
||||
results = []
|
||||
requests = []
|
||||
if commit:
|
||||
prompt = self._connection.get_prompt()
|
||||
if (b'(config-if' in prompt) or (b'(config' in prompt) or (b'(config-lag-if' in prompt):
|
||||
self.send_command('end')
|
||||
|
||||
self.send_command('configure terminal')
|
||||
|
||||
for line in to_list(candidate):
|
||||
if not isinstance(line, Mapping):
|
||||
line = {'command': line}
|
||||
|
||||
cmd = line['command']
|
||||
if cmd != 'end' and cmd[0] != '!':
|
||||
results.append(self.send_command(**line))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end')
|
||||
else:
|
||||
raise ValueError('check mode is not supported')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, check_all=False):
|
||||
if not command:
|
||||
raise ValueError('must provide value of command to execute')
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for get" % output)
|
||||
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all)
|
||||
|
||||
def scp(self, command=None, scp_user=None, scp_pass=None):
|
||||
if not command:
|
||||
raise ValueError('must provide value of command to execute')
|
||||
prompt = ["User name:", "Password:"]
|
||||
if(scp_pass is None):
|
||||
answer = [scp_user, self._connection._play_context.password]
|
||||
else:
|
||||
answer = [scp_user, scp_pass]
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=False, check_all=True)
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'icx'
|
||||
reply = self.get(command='show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1).strip(',')
|
||||
|
||||
match = re.search(r'^Cisco (.+) \(revision', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'^(.+) uptime', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_device_operations(self):
|
||||
return {
|
||||
'supports_diff_replace': True,
|
||||
'supports_commit': False,
|
||||
'supports_rollback': False,
|
||||
'supports_defaults': True,
|
||||
'supports_onbox_diff': False,
|
||||
'supports_commit_comment': False,
|
||||
'supports_multiline_delimiter': True,
|
||||
'supports_diff_match': True,
|
||||
'supports_diff_ignore_lines': True,
|
||||
'supports_generate_diff': True,
|
||||
'supports_replace': False
|
||||
}
|
||||
|
||||
def get_option_values(self):
|
||||
return {
|
||||
'format': ['text'],
|
||||
'diff_match': ['line', 'strict', 'exact', 'none'],
|
||||
'diff_replace': ['line', 'block'],
|
||||
'output': []
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = dict()
|
||||
result['rpc'] = self.get_base_rpc() + ['edit_banner', 'get_diff', 'run_commands', 'get_defaults_flag']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_operations'] = self.get_device_operations()
|
||||
result.update(self.get_option_values())
|
||||
return json.dumps(result)
|
||||
|
||||
def edit_banner(self, candidate=None, multiline_delimiter="@", commit=True):
|
||||
"""
|
||||
Edit banner on remote device
|
||||
:param banners: Banners to be loaded in json format
|
||||
:param multiline_delimiter: Line delimiter for banner
|
||||
:param commit: Boolean value that indicates if the device candidate
|
||||
configuration should be pushed in the running configuration or discarded.
|
||||
:param diff: Boolean flag to indicate if configuration that is applied on remote host should
|
||||
generated and returned in response or not
|
||||
:return: Returns response of executing the configuration command received
|
||||
from remote host
|
||||
"""
|
||||
resp = {}
|
||||
banners_obj = json.loads(candidate)
|
||||
results = []
|
||||
requests = []
|
||||
if commit:
|
||||
for key, value in iteritems(banners_obj):
|
||||
key += ' %s' % multiline_delimiter
|
||||
self.send_command('config terminal', sendonly=True)
|
||||
for cmd in [key, value, multiline_delimiter]:
|
||||
obj = {'command': cmd, 'sendonly': True}
|
||||
results.append(self.send_command(**obj))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end', sendonly=True)
|
||||
time.sleep(0.1)
|
||||
results.append(self.send_command('\n'))
|
||||
requests.append('\n')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
|
||||
return resp
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for run_commands" % output)
|
||||
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc:
|
||||
raise
|
||||
out = getattr(e, 'err', to_text(e))
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
|
||||
def _extract_banners(self, config):
|
||||
banners = {}
|
||||
banner_cmds = re.findall(r'^banner (\w+)', config, re.M)
|
||||
for cmd in banner_cmds:
|
||||
regex = r'banner %s \$(.+?)(?=\$)' % cmd
|
||||
match = re.search(regex, config, re.S)
|
||||
if match:
|
||||
key = 'banner %s' % cmd
|
||||
banners[key] = match.group(1).strip()
|
||||
|
||||
for cmd in banner_cmds:
|
||||
regex = r'banner %s \$(.+?)(?=\$)' % cmd
|
||||
match = re.search(regex, config, re.S)
|
||||
if match:
|
||||
config = config.replace(str(match.group(1)), '')
|
||||
|
||||
config = re.sub(r'banner \w+ \$\$', '!! banner removed', config)
|
||||
return config, banners
|
||||
|
||||
def _diff_banners(self, want, have):
|
||||
candidate = {}
|
||||
for key, value in iteritems(want):
|
||||
if value != have.get(key):
|
||||
candidate[key] = value
|
||||
return candidate
|
||||
95
plugins/cliconf/ironware.py
Normal file
95
plugins/cliconf/ironware.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: ironware
|
||||
short_description: Use ironware cliconf to run command on Extreme Ironware platform
|
||||
description:
|
||||
- This ironware plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Extreme Ironware network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'ironware'
|
||||
reply = self.send_command('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'IronWare : Version (\S+),', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'^(?:System Mode\:|System\:) (CES|CER|MLX|XMR)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if source == 'running':
|
||||
cmd = 'show running-config'
|
||||
if flags is not None:
|
||||
cmd += ' ' + ' '.join(flags)
|
||||
|
||||
else:
|
||||
cmd = 'show configuration'
|
||||
if flags is not None:
|
||||
raise ValueError("flags are only supported with running-config")
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
|
||||
def set_cli_prompt_context(self):
|
||||
"""
|
||||
Make sure we are in the operational cli mode
|
||||
:return: None
|
||||
"""
|
||||
if self._connection.connected:
|
||||
self._update_cli_prompt_context(config_context=')#')
|
||||
74
plugins/cliconf/netvisor.py
Normal file
74
plugins/cliconf/netvisor.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#
|
||||
# (c) 2016 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: netvisor
|
||||
short_description: Use netvisor cliconf to run command on Pluribus netvisor platform
|
||||
description:
|
||||
- This netvisor plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Pluribus netvisor devices.
|
||||
'''
|
||||
|
||||
import json
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
cmd = 'show running-config'
|
||||
return self.send_command(cmd)
|
||||
|
||||
def edit_config(self, command):
|
||||
return
|
||||
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, newline=True, check_all=False):
|
||||
if not command:
|
||||
raise ValueError('must provide value of command to execute')
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for get" % output)
|
||||
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_option_values(self):
|
||||
return {
|
||||
'format': ['text'],
|
||||
'diff_match': ['line', 'strict', 'exact', 'none'],
|
||||
'diff_replace': ['line', 'block'],
|
||||
'output': []
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = dict()
|
||||
result['rpc'] = self.get_base_rpc()
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
result.update(self.get_option_values())
|
||||
return json.dumps(result)
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
device_info['network_os'] = 'netvisor'
|
||||
|
||||
return device_info
|
||||
112
plugins/cliconf/nos.py
Normal file
112
plugins/cliconf/nos.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#
|
||||
# (c) 2018 Extreme Networks 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: nos
|
||||
short_description: Use nos cliconf to run command on Extreme NOS platform
|
||||
description:
|
||||
- This nos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Extreme NOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'nos'
|
||||
reply = self.get('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Network Operating System Version: (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
reply = self.get('show chassis')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'^Chassis Name:(\s+)(\S+)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(2)
|
||||
|
||||
reply = self.get('show running-config | inc "switch-attributes host-name"')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'switch-attributes host-name (\S+)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running', flags=None):
|
||||
if source not in 'running':
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
if source == 'running':
|
||||
cmd = 'show running-config'
|
||||
|
||||
flags = [] if flags is None else flags
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def edit_config(self, command):
|
||||
resp = {}
|
||||
results = []
|
||||
requests = []
|
||||
self.send_command('configure terminal')
|
||||
for cmd in to_list(command):
|
||||
if isinstance(cmd, dict):
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
newline = cmd.get('newline', True)
|
||||
else:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
newline = True
|
||||
|
||||
if cmd != 'end' and cmd[0] != '!':
|
||||
results.append(self.send_command(command, prompt, answer, False, newline))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
77
plugins/cliconf/onyx.py
Normal file
77
plugins/cliconf/onyx.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: onyx
|
||||
short_description: Use onyx cliconf to run command on Mellanox ONYX platform
|
||||
description:
|
||||
- This onyx plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Mellanox ONYX network devices.
|
||||
'''
|
||||
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
reply = self.get('show version | json-print')
|
||||
data = json.loads(reply)
|
||||
device_info['network_os'] = data['Product name']
|
||||
device_info['network_os_version'] = data['Product release']
|
||||
device_info['network_os_version_summary'] = data['Version summary']
|
||||
device_info['network_os_model'] = data['Product model']
|
||||
|
||||
reply = self.get('show hosts | include Hostname')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
hostname = data.split(':')[1]
|
||||
hostname = hostname.strip()
|
||||
device_info['network_os_hostname'] = hostname
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running',):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
cmd = 'show running-config'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['exit']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
78
plugins/cliconf/routeros.py
Normal file
78
plugins/cliconf/routeros.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#
|
||||
# (c) 2017 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: routeros
|
||||
short_description: Use routeros cliconf to run command on MikroTik RouterOS platform
|
||||
description:
|
||||
- This routeros plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from MikroTik RouterOS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
device_info['network_os'] = 'RouterOS'
|
||||
|
||||
resource = self.get('/system resource print')
|
||||
data = to_text(resource, errors='surrogate_or_strict').strip()
|
||||
match = re.search(r'version: (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
routerboard = self.get('/system routerboard print')
|
||||
data = to_text(routerboard, errors='surrogate_or_strict').strip()
|
||||
match = re.search(r'model: (.+)$', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
identity = self.get('/system identity print')
|
||||
data = to_text(identity, errors='surrogate_or_strict').strip()
|
||||
match = re.search(r'name: (.+)$', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
return
|
||||
|
||||
def edit_config(self, command):
|
||||
return
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
104
plugins/cliconf/slxos.py
Normal file
104
plugins/cliconf/slxos.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#
|
||||
# (c) 2018 Extreme Networks 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: slxos
|
||||
short_description: Use slxos cliconf to run command on Extreme SLX-OS platform
|
||||
description:
|
||||
- This slxos plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Extreme SLX-OS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'slxos'
|
||||
reply = self.get('show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'SLX\-OS Operating System Version: (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
reply = self.get('show chassis')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'^Chassis Name:(\s+)(\S+)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(2)
|
||||
|
||||
reply = self.get('show running-config | inc "switch-attributes host-name"')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'switch-attributes host-name (\S+)', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
if source == 'running':
|
||||
cmd = 'show running-config'
|
||||
else:
|
||||
cmd = 'show startup-config'
|
||||
|
||||
flags = [] if flags is None else flags
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
if isinstance(cmd, dict):
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
newline = cmd.get('newline', True)
|
||||
else:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
newline = True
|
||||
|
||||
self.send_command(command, prompt, answer, False, newline)
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
return json.dumps(result)
|
||||
235
plugins/cliconf/voss.py
Normal file
235
plugins/cliconf/voss.py
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
#
|
||||
# (c) 2018 Extreme Networks 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
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
cliconf: voss
|
||||
short_description: Use voss cliconf to run command on Extreme VOSS platform
|
||||
description:
|
||||
- This voss plugin provides low level abstraction apis for
|
||||
sending and receiving CLI commands from Extreme VOSS network devices.
|
||||
'''
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.common._collections_compat import Mapping
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps
|
||||
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
|
||||
from ansible_collections.community.general.plugins.module_utils.network.voss.voss import VossNetworkConfig
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', flags=None, format=None):
|
||||
if source not in ('running', 'startup'):
|
||||
raise ValueError("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if format:
|
||||
raise ValueError("'format' value %s is not supported for get_config" % format)
|
||||
|
||||
if not flags:
|
||||
flags = []
|
||||
if source == 'running':
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(to_list(flags))
|
||||
cmd = cmd.strip()
|
||||
else:
|
||||
cmd = 'more /intflash/config.cfg'
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
|
||||
"""
|
||||
Generate diff between candidate and running configuration. If the
|
||||
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
|
||||
candidate and running configurations are not required to be passed as argument.
|
||||
In case if onbox diff capability is not supported candidate argument is mandatory
|
||||
and running argument is optional.
|
||||
:param candidate: The configuration which is expected to be present on remote host.
|
||||
:param running: The base configuration which is used to generate diff.
|
||||
:param diff_match: Instructs how to match the candidate configuration with current device configuration
|
||||
Valid values are 'line', 'strict', 'exact', 'none'.
|
||||
'line' - commands are matched line by line
|
||||
'strict' - command lines are matched with respect to position
|
||||
'exact' - command lines must be an equal match
|
||||
'none' - will not compare the candidate configuration with the running configuration
|
||||
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
|
||||
ignored during the diff. This is used for lines in the configuration
|
||||
that are automatically updated by the system. This argument takes
|
||||
a list of regular expressions or exact line matches.
|
||||
:param path: The ordered set of parents that uniquely identify the section or hierarchy
|
||||
the commands should be checked against. If the parents argument
|
||||
is omitted, the commands are checked against the set of top
|
||||
level or global commands.
|
||||
:param diff_replace: Instructs on the way to perform the configuration on the device.
|
||||
If the replace argument is set to I(line) then the modified lines are
|
||||
pushed to the device in configuration mode. If the replace argument is
|
||||
set to I(block) then the entire command block is pushed to the device in
|
||||
configuration mode if any line is not correct.
|
||||
:return: Configuration diff in json format.
|
||||
{
|
||||
'config_diff': '',
|
||||
}
|
||||
|
||||
"""
|
||||
diff = {}
|
||||
|
||||
device_operations = self.get_device_operations()
|
||||
option_values = self.get_option_values()
|
||||
|
||||
if candidate is None and device_operations['supports_generate_diff']:
|
||||
raise ValueError("candidate configuration is required to generate diff")
|
||||
|
||||
if diff_match not in option_values['diff_match']:
|
||||
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
|
||||
|
||||
if diff_replace not in option_values['diff_replace']:
|
||||
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = VossNetworkConfig(indent=0, ignore_lines=diff_ignore_lines)
|
||||
candidate_obj.load(candidate)
|
||||
|
||||
if running and diff_match != 'none':
|
||||
# running configuration
|
||||
running_obj = VossNetworkConfig(indent=0, contents=running, ignore_lines=diff_ignore_lines)
|
||||
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
|
||||
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||
diff['diff_path'] = path
|
||||
diff['diff_replace'] = diff_replace
|
||||
return diff
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
|
||||
resp = {}
|
||||
operations = self.get_device_operations()
|
||||
self.check_edit_config_capability(operations, candidate, commit, replace, comment)
|
||||
|
||||
results = []
|
||||
requests = []
|
||||
if commit:
|
||||
self.send_command('configure terminal')
|
||||
for line in to_list(candidate):
|
||||
if not isinstance(line, Mapping):
|
||||
line = {'command': line}
|
||||
|
||||
cmd = line['command']
|
||||
if cmd != 'end' and cmd[0] != '!':
|
||||
results.append(self.send_command(**line))
|
||||
requests.append(cmd)
|
||||
|
||||
self.send_command('end')
|
||||
else:
|
||||
raise ValueError('check mode is not supported')
|
||||
|
||||
resp['request'] = requests
|
||||
resp['response'] = results
|
||||
return resp
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all)
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'voss'
|
||||
reply = self.get(command='show sys-info')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'SysDescr\s+: \S+ \((\S+)\)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'Chassis\s+: (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'SysName\s+: (\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_device_operations(self):
|
||||
return {
|
||||
'supports_diff_replace': True,
|
||||
'supports_commit': False,
|
||||
'supports_rollback': False,
|
||||
'supports_defaults': True,
|
||||
'supports_onbox_diff': False,
|
||||
'supports_commit_comment': False,
|
||||
'supports_multiline_delimiter': False,
|
||||
'supports_diff_match': True,
|
||||
'supports_diff_ignore_lines': True,
|
||||
'supports_generate_diff': True,
|
||||
'supports_replace': False
|
||||
}
|
||||
|
||||
def get_option_values(self):
|
||||
return {
|
||||
'format': ['text'],
|
||||
'diff_match': ['line', 'strict', 'exact', 'none'],
|
||||
'diff_replace': ['line', 'block'],
|
||||
'output': []
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = super(Cliconf, self).get_capabilities()
|
||||
result['rpc'] += ['get_diff', 'run_commands', 'get_defaults_flag']
|
||||
result['device_operations'] = self.get_device_operations()
|
||||
result.update(self.get_option_values())
|
||||
return json.dumps(result)
|
||||
|
||||
def run_commands(self, commands=None, check_rc=True):
|
||||
if commands is None:
|
||||
raise ValueError("'commands' value is required")
|
||||
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
if not isinstance(cmd, Mapping):
|
||||
cmd = {'command': cmd}
|
||||
|
||||
output = cmd.pop('output', None)
|
||||
if output:
|
||||
raise ValueError("'output' value %s is not supported for run_commands" % output)
|
||||
|
||||
try:
|
||||
out = self.send_command(**cmd)
|
||||
except AnsibleConnectionFailure as e:
|
||||
if check_rc:
|
||||
raise
|
||||
out = getattr(e, 'err', e)
|
||||
|
||||
responses.append(out)
|
||||
|
||||
return responses
|
||||
|
||||
def get_defaults_flag(self):
|
||||
return 'verbose'
|
||||
Loading…
Add table
Add a link
Reference in a new issue