mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-05-29 20:39:09 -07:00
nso_config module for setting configuration in Cisco NSO (#30973)
This commit is contained in:
parent
4443da2d79
commit
f1fe467c22
16 changed files with 1376 additions and 0 deletions
477
lib/ansible/module_utils/nso.py
Normal file
477
lib/ansible/module_utils/nso.py
Normal file
|
@ -0,0 +1,477 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017 Cisco and/or its affiliates.
|
||||
#
|
||||
# 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 ansible.module_utils.basic import env_fallback
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
nso_argument_spec = dict(
|
||||
url=dict(required=True),
|
||||
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME']), required=True),
|
||||
password=dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), required=True, no_log=True)
|
||||
)
|
||||
|
||||
|
||||
class State(object):
|
||||
SET = 'set'
|
||||
PRESENT = 'present'
|
||||
ABSENT = 'absent'
|
||||
CHECK_SYNC = 'check-sync'
|
||||
DEEP_CHECK_SYNC = 'deep-check-sync'
|
||||
IN_SYNC = 'in-sync'
|
||||
DEEP_IN_SYNC = 'deep-in-sync'
|
||||
|
||||
SYNC_STATES = ('check-sync', 'deep-check-sync', 'in-sync', 'deep-in-sync')
|
||||
|
||||
|
||||
class ModuleFailException(Exception):
|
||||
def __init__(self, message):
|
||||
super(ModuleFailException, self).__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class NsoException(Exception):
|
||||
def __init__(self, message, error):
|
||||
super(NsoException, self).__init__(message)
|
||||
self.message = message
|
||||
self.error = error
|
||||
|
||||
|
||||
class JsonRpc(object):
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
|
||||
self._id = 0
|
||||
self._trans = {}
|
||||
self._headers = {'Content-Type': 'application/json'}
|
||||
self._conn = None
|
||||
|
||||
def login(self, user, passwd):
|
||||
payload = {
|
||||
'method': 'login',
|
||||
'params': {'user': user, 'passwd': passwd}
|
||||
}
|
||||
resp, resp_json = self._call(payload)
|
||||
self._headers['Cookie'] = resp.headers['set-cookie']
|
||||
|
||||
def logout(self):
|
||||
payload = {'method': 'logout', 'params': {}}
|
||||
self._call(payload)
|
||||
|
||||
def get_system_setting(self, setting):
|
||||
payload = {'method': 'get_system_setting', 'params': {'operation': setting}}
|
||||
resp, resp_json = self._call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def new_trans(self, **kwargs):
|
||||
payload = {'method': 'new_trans', 'params': kwargs}
|
||||
resp, resp_json = self._call(payload)
|
||||
return resp_json['result']['th']
|
||||
|
||||
def delete_trans(self, th):
|
||||
payload = {'method': 'delete_trans', 'params': {'th': th}}
|
||||
resp, resp_json = self._call(payload)
|
||||
|
||||
def validate_trans(self, th):
|
||||
payload = {'method': 'validate_trans', 'params': {'th': th}}
|
||||
resp, resp_json = self._write_call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def get_trans_changes(self, th):
|
||||
payload = {'method': 'get_trans_changes', 'params': {'th': th}}
|
||||
resp, resp_json = self._write_call(payload)
|
||||
return resp_json['result']['changes']
|
||||
|
||||
def validate_commit(self, th):
|
||||
payload = {'method': 'validate_commit', 'params': {'th': th}}
|
||||
resp, resp_json = self._write_call(payload)
|
||||
return resp_json['result'].get('warnings', [])
|
||||
|
||||
def commit(self, th):
|
||||
payload = {'method': 'commit', 'params': {'th': th}}
|
||||
resp, resp_json = self._write_call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def get_schema(self, **kwargs):
|
||||
payload = {'method': 'get_schema', 'params': kwargs}
|
||||
resp, resp_json = self._read_call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def get_module_prefix_map(self):
|
||||
payload = {'method': 'get_module_prefix_map', 'params': {}}
|
||||
resp, resp_json = self._call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def get_value(self, path):
|
||||
payload = {
|
||||
'method': 'get_value',
|
||||
'params': {'path': path}
|
||||
}
|
||||
resp, resp_json = self._read_call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def exists(self, path):
|
||||
payload = {'method': 'exists', 'params': {'path': path}}
|
||||
resp, resp_json = self._read_call(payload)
|
||||
return resp_json['result']['exists']
|
||||
|
||||
def create(self, th, path):
|
||||
payload = {'method': 'create', 'params': {'th': th, 'path': path}}
|
||||
self._write_call(payload)
|
||||
|
||||
def delete(self, th, path):
|
||||
payload = {'method': 'delete', 'params': {'th': th, 'path': path}}
|
||||
self._write_call(payload)
|
||||
|
||||
def set_value(self, th, path, value):
|
||||
payload = {
|
||||
'method': 'set_value',
|
||||
'params': {'th': th, 'path': path, 'value': value}
|
||||
}
|
||||
resp, resp_json = self._write_call(payload)
|
||||
return resp_json['result']
|
||||
|
||||
def run_action(self, th, path, params=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
payload = {
|
||||
'method': 'run_action',
|
||||
'params': {
|
||||
'format': 'normal',
|
||||
'path': path,
|
||||
'params': params
|
||||
}
|
||||
}
|
||||
if th is None:
|
||||
resp, resp_json = self._read_call(payload)
|
||||
else:
|
||||
payload['params']['th'] = th
|
||||
resp, resp_json = self._call(payload)
|
||||
|
||||
return resp_json['result']
|
||||
|
||||
def _call(self, payload):
|
||||
self._id += 1
|
||||
if 'id' not in payload:
|
||||
payload['id'] = self._id
|
||||
|
||||
if 'jsonrpc' not in payload:
|
||||
payload['jsonrpc'] = '2.0'
|
||||
|
||||
data = json.dumps(payload)
|
||||
resp = open_url(
|
||||
self._url, method='POST', data=data, headers=self._headers)
|
||||
if resp.code != 200:
|
||||
raise NsoException(
|
||||
'NSO returned HTTP code {0}, expected 200'.format(resp.status), {})
|
||||
|
||||
resp_body = resp.read()
|
||||
resp_json = json.loads(resp_body)
|
||||
|
||||
if 'error' in resp_json:
|
||||
self._handle_call_error(payload, resp_json)
|
||||
return resp, resp_json
|
||||
|
||||
def _handle_call_error(self, payload, resp_json):
|
||||
method = payload['method']
|
||||
|
||||
error = resp_json['error']
|
||||
error_type = error['type'][len('rpc.method.'):]
|
||||
if error_type in ('unexpected_params',
|
||||
'unknown_params_value',
|
||||
'invalid_params',
|
||||
'invalid_params_type',
|
||||
'data_not_found'):
|
||||
key = error['data']['param']
|
||||
error_type_s = error_type.replace('_', ' ')
|
||||
if key == 'path':
|
||||
msg = 'NSO {0} {1}. path = {2}'.format(
|
||||
method, error_type_s, payload['params']['path'])
|
||||
else:
|
||||
path = payload['params'].get('path', 'unknown')
|
||||
msg = 'NSO {0} {1}. path = {2}. {3} = {4}'.format(
|
||||
method, error_type_s, path, key, payload['params'][key])
|
||||
else:
|
||||
msg = 'NSO {0} returned JSON-RPC error: {1}'.format(method, error)
|
||||
|
||||
raise NsoException(msg, error)
|
||||
|
||||
def _read_call(self, payload):
|
||||
if 'th' not in payload['params']:
|
||||
payload['params']['th'] = self._get_th(mode='read')
|
||||
return self._call(payload)
|
||||
|
||||
def _write_call(self, payload):
|
||||
if 'th' not in payload['params']:
|
||||
payload['params']['th'] = self._get_th(mode='read_write')
|
||||
return self._call(payload)
|
||||
|
||||
def _get_th(self, mode='read'):
|
||||
if mode not in self._trans:
|
||||
th = self.new_trans(mode=mode)
|
||||
self._trans[mode] = th
|
||||
return self._trans[mode]
|
||||
|
||||
|
||||
class ValueBuilder(object):
|
||||
class Value(object):
|
||||
__slots__ = ['path', 'state', 'value']
|
||||
|
||||
def __init__(self, path, state, value):
|
||||
self.path = path
|
||||
self.state = state
|
||||
self.value = value
|
||||
|
||||
def __lt__(self, rhs):
|
||||
l_len = len(self.path.split('/'))
|
||||
r_len = len(rhs.path.split('/'))
|
||||
if l_len == r_len:
|
||||
return self.path.__lt__(rhs.path)
|
||||
return l_len < r_len
|
||||
|
||||
def __str__(self):
|
||||
return 'Value<path={0}, state={1}, value={2}>'.format(
|
||||
self.path, self.state, self.value)
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
self._schema_cache = {}
|
||||
self._module_prefix_map_cache = None
|
||||
self._values = []
|
||||
self._values_dirty = False
|
||||
self._path_re = re.compile('{[^}]*}')
|
||||
|
||||
def build(self, parent, maybe_qname, value, schema=None):
|
||||
qname, name = self._get_prefix_name(maybe_qname)
|
||||
if name is None:
|
||||
path = parent
|
||||
else:
|
||||
path = '{0}/{1}'.format(parent, qname)
|
||||
|
||||
if schema is None:
|
||||
schema = self._get_schema(path)
|
||||
|
||||
if self._is_leaf(schema):
|
||||
if self._is_empty_leaf(schema):
|
||||
exists = self._client.exists(path)
|
||||
if exists and value != [None]:
|
||||
self._add_value(path, State.ABSENT, None)
|
||||
elif not exists and value == [None]:
|
||||
self._add_value(path, State.PRESENT, None)
|
||||
else:
|
||||
value_type = self._get_type(parent, maybe_qname)
|
||||
if value_type == 'identityref':
|
||||
value, t_value = self._get_prefix_name(value)
|
||||
self._add_value(path, State.SET, value)
|
||||
elif isinstance(value, dict):
|
||||
self._build_dict(path, schema, value)
|
||||
elif isinstance(value, list):
|
||||
self._build_list(path, schema, value)
|
||||
else:
|
||||
raise ModuleFailException(
|
||||
'unsupported schema {0} at {1}'.format(
|
||||
schema['kind'], path))
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
if self._values_dirty:
|
||||
self._values.sort()
|
||||
self._values_dirty = False
|
||||
|
||||
return self._values
|
||||
|
||||
def _build_dict(self, path, schema, value):
|
||||
keys = schema.get('key', [])
|
||||
for dict_key, dict_value in value.items():
|
||||
qname, name = self._get_prefix_name(dict_key)
|
||||
if dict_key in ('__state', ) or name in keys:
|
||||
continue
|
||||
|
||||
child_schema = self._find_child(path, schema, qname)
|
||||
self.build(path, dict_key, dict_value, child_schema)
|
||||
|
||||
def _build_list(self, path, schema, value):
|
||||
for entry in value:
|
||||
entry_key = self._build_key(path, entry, schema['key'])
|
||||
entry_path = '{0}{{{1}}}'.format(path, entry_key)
|
||||
entry_state = entry.get('__state', 'present')
|
||||
entry_exists = self._client.exists(entry_path)
|
||||
|
||||
if entry_state == 'absent':
|
||||
if entry_exists:
|
||||
self._add_value(entry_path, State.ABSENT, None)
|
||||
else:
|
||||
if not entry_exists:
|
||||
self._add_value(entry_path, State.PRESENT, None)
|
||||
if entry_state in State.SYNC_STATES:
|
||||
self._add_value(entry_path, entry_state, None)
|
||||
|
||||
self.build(entry_path, None, entry)
|
||||
|
||||
def _build_key(self, path, entry, schema_keys):
|
||||
key_parts = []
|
||||
for key in schema_keys:
|
||||
value = entry.get(key, None)
|
||||
if value is None:
|
||||
raise ModuleFailException(
|
||||
'required leaf {0} in {1} not set in data'.format(
|
||||
key, path))
|
||||
|
||||
value_type = self._get_type(path, key)
|
||||
if value_type == 'identityref':
|
||||
value, t_value = self._get_prefix_name(value)
|
||||
key_parts.append(self._quote_key(value))
|
||||
return ' '.join(key_parts)
|
||||
|
||||
def _quote_key(self, key):
|
||||
if isinstance(key, bool):
|
||||
return key and 'true' or 'false'
|
||||
|
||||
q_key = []
|
||||
for c in str(key):
|
||||
if c in ('{', '}', "'", '\\'):
|
||||
q_key.append('\\')
|
||||
q_key.append(c)
|
||||
q_key = ''.join(q_key)
|
||||
if ' ' in q_key:
|
||||
return '{0}'.format(q_key)
|
||||
return q_key
|
||||
|
||||
def _find_child(self, path, schema, qname):
|
||||
if 'children' not in schema:
|
||||
schema = self._get_schema(path)
|
||||
|
||||
# look for the qualified name if : is in the name
|
||||
child_schema = self._get_child(schema, qname)
|
||||
if child_schema is not None:
|
||||
return child_schema
|
||||
|
||||
# no child was found, look for a choice with a child matching
|
||||
for child_schema in schema['children']:
|
||||
if child_schema['kind'] != 'choice':
|
||||
continue
|
||||
choice_child_schema = self._get_choice_child(child_schema, qname)
|
||||
if choice_child_schema is not None:
|
||||
return choice_child_schema
|
||||
|
||||
raise ModuleFailException(
|
||||
'no child in {0} with name {1}. children {2}'.format(
|
||||
path, qname, ','.join((c.get('qname', c.get('name', None)) for c in schema['children']))))
|
||||
|
||||
def _add_value(self, path, state, value):
|
||||
self._values.append(ValueBuilder.Value(path, state, value))
|
||||
self._values_dirty = True
|
||||
|
||||
def _get_prefix_name(self, qname):
|
||||
if qname is None:
|
||||
return None, None
|
||||
if ':' not in qname:
|
||||
return qname, qname
|
||||
|
||||
module_prefix_map = self._get_module_prefix_map()
|
||||
module, name = qname.split(':', 1)
|
||||
if module not in module_prefix_map:
|
||||
raise ModuleFailException(
|
||||
'no module mapping for module {0}. loaded modules {1}'.format(
|
||||
module, ','.join(sorted(module_prefix_map.keys()))))
|
||||
|
||||
return '{0}:{1}'.format(module_prefix_map[module], name), name
|
||||
|
||||
def _get_schema(self, path):
|
||||
return self._ensure_schema_cached(path)['data']
|
||||
|
||||
def _get_type(self, parent_path, key):
|
||||
all_schema = self._ensure_schema_cached(parent_path)
|
||||
parent_schema = all_schema['data']
|
||||
meta = all_schema['meta']
|
||||
|
||||
schema = self._get_child(parent_schema, key)
|
||||
if self._is_leaf(schema):
|
||||
path_type = schema['type']
|
||||
if path_type.get('primitive', False):
|
||||
return path_type['name']
|
||||
else:
|
||||
path_type_key = '{0}:{1}'.format(
|
||||
path_type['namespace'], path_type['name'])
|
||||
type_info = meta['types'][path_type_key]
|
||||
return type_info[-1]['name']
|
||||
return None
|
||||
|
||||
def _ensure_schema_cached(self, path):
|
||||
path = self._path_re.sub('', path)
|
||||
if path not in self._schema_cache:
|
||||
schema = self._client.get_schema(path=path, levels=1)
|
||||
self._schema_cache[path] = schema
|
||||
return self._schema_cache[path]
|
||||
|
||||
def _get_module_prefix_map(self):
|
||||
if self._module_prefix_map_cache is None:
|
||||
self._module_prefix_map_cache = self._client.get_module_prefix_map()
|
||||
return self._module_prefix_map_cache
|
||||
|
||||
def _get_child(self, schema, qname):
|
||||
# no child specified, return parent
|
||||
if qname is None:
|
||||
return schema
|
||||
|
||||
name_key = ':' in qname and 'qname' or 'name'
|
||||
return next((c for c in schema['children']
|
||||
if c.get(name_key, None) == qname), None)
|
||||
|
||||
def _get_choice_child(self, schema, qname):
|
||||
name_key = ':' in qname and 'qname' or 'name'
|
||||
for child_case in schema['cases']:
|
||||
choice_child_schema = next(
|
||||
(c for c in child_case['children']
|
||||
if c.get(name_key, None) == qname), None)
|
||||
if choice_child_schema is not None:
|
||||
return choice_child_schema
|
||||
return None
|
||||
|
||||
def _is_leaf(self, schema):
|
||||
return schema.get('kind', None) in ('key', 'leaf', 'leaf-list')
|
||||
|
||||
def _is_empty_leaf(self, schema):
|
||||
return (schema.get('kind', None) == 'leaf' and
|
||||
schema['type'].get('primitive', False) and
|
||||
schema['type'].get('name', '') == 'empty')
|
||||
|
||||
|
||||
def connect(params):
|
||||
client = JsonRpc(params['url'])
|
||||
client.login(params['username'], params['password'])
|
||||
return client
|
||||
|
||||
|
||||
def verify_version(client):
|
||||
version_str = client.get_system_setting('version')
|
||||
version = [int(p) for p in version_str.split('.')]
|
||||
if len(version) < 2:
|
||||
raise ModuleFailException(
|
||||
'unsupported NSO version format {0}'.format(version_str))
|
||||
if (version[0] < 4 or version[1] < 4 or
|
||||
(version[1] == 4 and (len(version) < 3 or version[2] < 3))):
|
||||
raise ModuleFailException(
|
||||
'unsupported NSO version {0}, only 4.4.3 or later is supported'.format(version_str))
|
Loading…
Add table
Add a link
Reference in a new issue