mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 19:31:26 -07:00
IOS-XR NetConf and Cliconf plugin work (#33332)
* - Netconf plugin addition for iosxr - Utilities refactoring to support netconf and cliconf - iosx_banner refactoring for netconf and cliconf - Integration testcases changes to accomodate above changes * Fix sanity failures, shippable errors and review comments * fix pep8 issue * changes run_command method to send specific command args * - Review comment fixes - iosxr_command changes to remove ComplexDict based command_spec * - Move namespaces removal method from utils to netconf plugin * Minor refactoring in utils and change in deprecation message * rewrite build_xml logic and import changes for new utils dir structure * - Review comment changes and minor changes to documentation * * refactor common code and docs updates
This commit is contained in:
parent
4b6061ce3e
commit
2bc4c4f156
42 changed files with 1090 additions and 247 deletions
|
@ -26,12 +26,44 @@
|
|||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import env_fallback, return_values
|
||||
from ansible.module_utils.network.common.utils import to_list, ComplexList
|
||||
from ansible.module_utils.connection import exec_command
|
||||
import json
|
||||
from difflib import Differ
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
from ansible.module_utils.network.common.utils import to_list
|
||||
from ansible.module_utils.connection import Connection
|
||||
from ansible.module_utils.network.common.netconf import NetconfConnection
|
||||
|
||||
try:
|
||||
from ncclient.xml_ import to_xml
|
||||
HAS_NCCLIENT = True
|
||||
except ImportError:
|
||||
HAS_NCCLIENT = False
|
||||
|
||||
try:
|
||||
from lxml import etree
|
||||
HAS_XML = True
|
||||
except ImportError:
|
||||
HAS_XML = False
|
||||
|
||||
_DEVICE_CONFIGS = {}
|
||||
_EDIT_OPS = frozenset(['merge', 'create', 'replace', 'delete'])
|
||||
|
||||
BASE_1_0 = "{urn:ietf:params:xml:ns:netconf:base:1.0}"
|
||||
|
||||
NS_DICT = {
|
||||
'BASE_NSMAP': {"xc": "urn:ietf:params:xml:ns:netconf:base:1.0"},
|
||||
'BANNERS_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg"},
|
||||
'INTERFACES_NSMAP': {None: "http://openconfig.net/yang/interfaces"},
|
||||
'INSTALL_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-installmgr-admin-oper"},
|
||||
'HOST-NAMES_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-shellutil-cfg"},
|
||||
'M:TYPE_NSMAP': {"idx": "urn:ietf:params:xml:ns:yang:iana-if-type"},
|
||||
'ETHERNET_NSMAP': {None: "http://openconfig.net/yang/interfaces/ethernet"},
|
||||
'CETHERNET_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-drivers-media-eth-cfg"},
|
||||
'INTERFACE-CONFIGURATIONS_NSMAP': {None: "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"}
|
||||
}
|
||||
|
||||
iosxr_provider_spec = {
|
||||
'host': dict(),
|
||||
|
@ -40,10 +72,19 @@ iosxr_provider_spec = {
|
|||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
|
||||
'timeout': dict(type='int'),
|
||||
'transport': dict(),
|
||||
}
|
||||
|
||||
iosxr_argument_spec = {
|
||||
'provider': dict(type='dict', options=iosxr_provider_spec)
|
||||
}
|
||||
|
||||
command_spec = {
|
||||
'command': dict(),
|
||||
'prompt': dict(default=None),
|
||||
'answer': dict(default=None)
|
||||
}
|
||||
|
||||
iosxr_top_spec = {
|
||||
'host': dict(removed_in_version=2.9),
|
||||
'port': dict(removed_in_version=2.9, type='int'),
|
||||
|
@ -59,91 +100,317 @@ def get_provider_argspec():
|
|||
return iosxr_provider_spec
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
pass
|
||||
def get_connection(module):
|
||||
if hasattr(module, 'connection'):
|
||||
return module.connection
|
||||
|
||||
capabilities = get_device_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api == 'cliconf':
|
||||
module.connection = Connection(module._socket_path)
|
||||
elif network_api == 'netconf':
|
||||
module.connection = NetconfConnection(module._socket_path)
|
||||
else:
|
||||
module.fail_json(msg='Invalid connection type {!s}'.format(network_api))
|
||||
|
||||
return module.connection
|
||||
|
||||
|
||||
def get_config(module, flags=None):
|
||||
flags = [] if flags is None else flags
|
||||
def get_device_capabilities(module):
|
||||
if hasattr(module, 'capabilities'):
|
||||
return module.capabilities
|
||||
|
||||
cmd = 'show running-config '
|
||||
cmd += ' '.join(flags)
|
||||
cmd = cmd.strip()
|
||||
capabilities = Connection(module._socket_path).get_capabilities()
|
||||
module.capabilities = json.loads(capabilities)
|
||||
|
||||
try:
|
||||
return _DEVICE_CONFIGS[cmd]
|
||||
except KeyError:
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to retrieve current config', stderr=to_text(err, errors='surrogate_or_strict'))
|
||||
cfg = to_text(out, errors='surrogate_or_strict').strip()
|
||||
_DEVICE_CONFIGS[cmd] = cfg
|
||||
return module.capabilities
|
||||
|
||||
|
||||
def build_xml_subtree(container_ele, xmap, param=None, opcode=None):
|
||||
sub_root = container_ele
|
||||
meta_subtree = list()
|
||||
|
||||
for key, meta in xmap.items():
|
||||
|
||||
candidates = meta.get('xpath', "").split("/")
|
||||
|
||||
if container_ele.tag == candidates[-2]:
|
||||
parent = container_ele
|
||||
elif sub_root.tag == candidates[-2]:
|
||||
parent = sub_root
|
||||
else:
|
||||
parent = sub_root.find(".//" + meta.get('xpath', "").split(sub_root.tag + '/', 1)[1].rsplit('/', 1)[0])
|
||||
|
||||
if ((opcode in ('delete', 'merge') and meta.get('operation', 'unknown') == 'edit') or
|
||||
meta.get('operation', None) is None):
|
||||
|
||||
if meta.get('tag', False):
|
||||
if parent.tag == container_ele.tag:
|
||||
if meta.get('ns', None) is True:
|
||||
child = etree.Element(candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"])
|
||||
else:
|
||||
child = etree.Element(candidates[-1])
|
||||
meta_subtree.append(child)
|
||||
sub_root = child
|
||||
else:
|
||||
if meta.get('ns', None) is True:
|
||||
child = etree.SubElement(parent, candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"])
|
||||
else:
|
||||
child = etree.SubElement(parent, candidates[-1])
|
||||
|
||||
if meta.get('attrib', None) and opcode in ('delete', 'merge'):
|
||||
child.set(BASE_1_0 + meta.get('attrib'), opcode)
|
||||
|
||||
continue
|
||||
|
||||
text = None
|
||||
param_key = key.split(":")
|
||||
if param_key[0] == 'a':
|
||||
if param.get(param_key[1], None):
|
||||
text = param.get(param_key[1])
|
||||
elif param_key[0] == 'm':
|
||||
if meta.get('value', None):
|
||||
text = meta.get('value')
|
||||
|
||||
if text:
|
||||
if meta.get('ns', None) is True:
|
||||
child = etree.SubElement(parent, candidates[-1], nsmap=NS_DICT[key.upper() + "_NSMAP"])
|
||||
else:
|
||||
child = etree.SubElement(parent, candidates[-1])
|
||||
child.text = text
|
||||
|
||||
if len(meta_subtree) > 1:
|
||||
for item in meta_subtree:
|
||||
container_ele.append(item)
|
||||
|
||||
return sub_root
|
||||
|
||||
|
||||
def build_xml(container, xmap=None, params=None, opcode=None):
|
||||
|
||||
'''
|
||||
Builds netconf xml rpc document from meta-data
|
||||
|
||||
Args:
|
||||
container: the YANG container within the namespace
|
||||
xmap: meta-data map to build xml tree
|
||||
params: Input params that feed xml tree values
|
||||
opcode: operation to be performed (merge, delete etc.)
|
||||
|
||||
Example:
|
||||
Module inputs:
|
||||
banner_params = [{'banner':'motd', 'text':'Ansible banner example', 'state':'present'}]
|
||||
|
||||
Meta-data definition:
|
||||
bannermap = collections.OrderedDict()
|
||||
bannermap.update([
|
||||
('banner', {'xpath' : 'banners/banner', 'tag' : True, 'attrib' : "operation"}),
|
||||
('a:banner', {'xpath' : 'banner/banner-name'}),
|
||||
('a:text', {'xpath' : 'banner/banner-text', 'operation' : 'edit'})
|
||||
])
|
||||
|
||||
Fields:
|
||||
key: exact match to the key in arg_spec for a parameter
|
||||
(prefixes --> a: value fetched from arg_spec, m: value fetched from meta-data)
|
||||
xpath: xpath of the element (based on YANG model)
|
||||
tag: True if no text on the element
|
||||
attrib: attribute to be embedded in the element (e.g. xc:operation="merge")
|
||||
operation: if edit --> includes the element in edit_config() query else ignores for get() queries
|
||||
value: if key is prefixed with "m:", value is required in meta-data
|
||||
|
||||
Output:
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<banners xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg">
|
||||
<banner xc:operation="merge">
|
||||
<banner-name>motd</banner-name>
|
||||
<banner-text>Ansible banner example</banner-text>
|
||||
</banner>
|
||||
</banners>
|
||||
</config>
|
||||
:returns: xml rpc document as a string
|
||||
'''
|
||||
|
||||
if opcode == 'filter':
|
||||
root = etree.Element("filter", type="subtree")
|
||||
elif opcode in ('delete', 'merge'):
|
||||
root = etree.Element("config", nsmap=NS_DICT['BASE_NSMAP'])
|
||||
|
||||
container_ele = etree.SubElement(root, container, nsmap=NS_DICT[container.upper() + "_NSMAP"])
|
||||
|
||||
if xmap:
|
||||
if not params:
|
||||
build_xml_subtree(container_ele, xmap)
|
||||
else:
|
||||
subtree_list = list()
|
||||
|
||||
for param in to_list(params):
|
||||
subtree_list.append(build_xml_subtree(container_ele, xmap, param, opcode=opcode))
|
||||
|
||||
for item in subtree_list:
|
||||
container_ele.append(item)
|
||||
|
||||
return etree.tostring(root)
|
||||
|
||||
|
||||
def etree_find(root, node):
|
||||
element = etree.fromstring(root).find('.//' + to_bytes(node, errors='surrogate_then_replace').strip())
|
||||
if element is not None:
|
||||
return element
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def etree_findall(root, node):
|
||||
element = etree.fromstring(root).findall('.//' + to_bytes(node, errors='surrogate_then_replace').strip())
|
||||
if element is not None:
|
||||
return element
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_cliconf(module):
|
||||
capabilities = get_device_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api not in ('cliconf', 'netconf'):
|
||||
module.fail_json(msg=('unsupported network_api: {!s}'.format(network_api)))
|
||||
return False
|
||||
|
||||
if network_api == 'cliconf':
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_netconf(module):
|
||||
capabilities = get_device_capabilities(module)
|
||||
network_api = capabilities.get('network_api')
|
||||
if network_api not in ('cliconf', 'netconf'):
|
||||
module.fail_json(msg=('unsupported network_api: {!s}'.format(network_api)))
|
||||
return False
|
||||
|
||||
if network_api == 'netconf':
|
||||
if not HAS_NCCLIENT:
|
||||
module.fail_json(msg=('ncclient is not installed'))
|
||||
if not HAS_XML:
|
||||
module.fail_json(msg=('lxml is not installed'))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_config_diff(module, running=None, candidate=None):
|
||||
conn = get_connection(module)
|
||||
|
||||
if is_cliconf(module):
|
||||
return conn.get('show commit changes diff')
|
||||
elif is_netconf(module):
|
||||
if running and candidate:
|
||||
running_data = running.split("\n", 1)[1].rsplit("\n", 1)[0]
|
||||
candidate_data = candidate.split("\n", 1)[1].rsplit("\n", 1)[0]
|
||||
if running_data != candidate_data:
|
||||
d = Differ()
|
||||
diff = list(d.compare(running_data.splitlines(), candidate_data.splitlines()))
|
||||
return '\n'.join(diff).strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def discard_config(module):
|
||||
conn = get_connection(module)
|
||||
conn.discard_changes()
|
||||
|
||||
|
||||
def commit_config(module, comment=None, confirmed=False, confirm_timeout=None, persist=False, check=False):
|
||||
conn = get_connection(module)
|
||||
reply = None
|
||||
|
||||
if check:
|
||||
reply = conn.validate()
|
||||
else:
|
||||
if is_netconf(module):
|
||||
reply = conn.commit(confirmed=confirmed, timeout=confirm_timeout, persist=persist)
|
||||
elif is_cliconf(module):
|
||||
reply = conn.commit(comment=comment)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
def get_config(module, source='running', config_filter=None):
|
||||
global _DEVICE_CONFIGS
|
||||
|
||||
conn = get_connection(module)
|
||||
|
||||
if config_filter is not None:
|
||||
key = (source + ' ' + ' '.join(config_filter)).strip().rstrip()
|
||||
else:
|
||||
key = source
|
||||
config = _DEVICE_CONFIGS.get(key)
|
||||
if config:
|
||||
return config
|
||||
else:
|
||||
out = conn.get_config(source=source, filter=config_filter)
|
||||
if is_netconf(module):
|
||||
out = to_xml(conn.get_config(source=source, filter=config_filter))
|
||||
|
||||
cfg = to_bytes(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS.update({key: cfg})
|
||||
return cfg
|
||||
|
||||
|
||||
def to_commands(module, commands):
|
||||
spec = {
|
||||
'command': dict(key=True),
|
||||
'prompt': dict(),
|
||||
'answer': dict()
|
||||
}
|
||||
transform = ComplexList(spec, module)
|
||||
return transform(commands)
|
||||
def load_config(module, command_filter, warnings, replace=False, admin=False, commit=False, comment=None):
|
||||
conn = get_connection(module)
|
||||
|
||||
if is_netconf(module):
|
||||
# FIXME: check for platform behaviour and restore this
|
||||
# ret = conn.lock(target = 'candidate')
|
||||
# ret = conn.discard_changes()
|
||||
try:
|
||||
ret = conn.edit_config(command_filter)
|
||||
finally:
|
||||
# ret = conn.unlock(target = 'candidate')
|
||||
pass
|
||||
|
||||
def run_commands(module, commands, check_rc=True):
|
||||
responses = list()
|
||||
commands = to_commands(module, to_list(commands))
|
||||
for cmd in to_list(commands):
|
||||
cmd = module.jsonify(cmd)
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if check_rc and rc != 0:
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_or_strict'), rc=rc)
|
||||
responses.append(to_text(out, errors='surrogate_or_strict'))
|
||||
return responses
|
||||
return ret
|
||||
|
||||
|
||||
def load_config(module, commands, warnings, commit=False, replace=False, comment=None, admin=False):
|
||||
cmd = 'configure terminal'
|
||||
if admin:
|
||||
cmd = 'admin ' + cmd
|
||||
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg='unable to enter configuration mode', err=to_text(err, errors='surrogate_or_strict'))
|
||||
|
||||
failed = False
|
||||
for command in to_list(commands):
|
||||
if command == 'end':
|
||||
continue
|
||||
|
||||
rc, out, err = exec_command(module, command)
|
||||
if rc != 0:
|
||||
failed = True
|
||||
break
|
||||
|
||||
if failed:
|
||||
exec_command(module, 'abort')
|
||||
module.fail_json(msg=to_text(err, errors='surrogate_or_strict'), commands=commands, rc=rc)
|
||||
|
||||
rc, diff, err = exec_command(module, 'show commit changes diff')
|
||||
if rc != 0:
|
||||
# If we failed, maybe we are in an old version so
|
||||
# we run show configuration instead
|
||||
rc, diff, err = exec_command(module, 'show configuration')
|
||||
elif is_cliconf(module):
|
||||
# to keep the pre-cliconf behaviour, make a copy, avoid adding commands to input list
|
||||
cmd_filter = deepcopy(command_filter)
|
||||
cmd_filter.insert(0, 'configure terminal')
|
||||
if admin:
|
||||
cmd_filter.insert(0, 'admin')
|
||||
conn.edit_config(cmd_filter)
|
||||
diff = get_config_diff(module)
|
||||
if module._diff:
|
||||
warnings.append('device platform does not support config diff')
|
||||
if diff:
|
||||
module['diff'] = to_text(diff, errors='surrogate_or_strict')
|
||||
if commit:
|
||||
commit_config(module, comment=comment)
|
||||
conn.edit_config('end')
|
||||
else:
|
||||
conn.discard_changes()
|
||||
|
||||
if commit:
|
||||
cmd = 'commit'
|
||||
if comment:
|
||||
cmd += ' comment {0}'.format(comment)
|
||||
else:
|
||||
cmd = 'abort'
|
||||
return diff
|
||||
|
||||
rc, out, err = exec_command(module, cmd)
|
||||
if rc != 0:
|
||||
exec_command(module, 'abort')
|
||||
module.fail_json(msg=err, commands=commands, rc=rc)
|
||||
|
||||
return to_text(diff, errors='surrogate_or_strict')
|
||||
def run_command(module, commands):
|
||||
conn = get_connection(module)
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
try:
|
||||
cmd = json.loads(cmd)
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
except:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
|
||||
out = conn.get(command, prompt, answer)
|
||||
|
||||
try:
|
||||
responses.append(to_text(out, errors='surrogate_or_strict'))
|
||||
except UnicodeError:
|
||||
module.fail_json(msg=u'failed to decode output from {0}:{1}'.format(cmd, to_text(out)))
|
||||
return responses
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue