nso_config module for setting configuration in Cisco NSO (#30973)

This commit is contained in:
cnasten 2017-11-09 14:34:41 +01:00 committed by John R Barker
parent 4443da2d79
commit f1fe467c22
16 changed files with 1376 additions and 0 deletions

5
.github/BOTMETA.yml vendored
View file

@ -470,6 +470,7 @@ files:
$modules/network/netvisor/: $team_netvisor
$modules/network/nuage/: pdellaert
$modules/network/nxos/: $team_nxos
$modules/network/nso/: $team_nso
$modules/network/openvswitch/:
ignored: stygstra
maintainers: $team_networking
@ -998,6 +999,9 @@ files:
$module_utils/network.py:
maintainers: $team_networking
labels: networking
$module_utils/nso.py:
maintainers: $team_nso
labels: networking
$module_utils/nxos.py:
maintainers: $team_networking
labels:
@ -1205,6 +1209,7 @@ macros:
team_netscaler: chiradeep giorgos-nikolopoulos
team_netvisor: Qalthos amitsi gundalow privateip
team_networking: Qalthos ganeshrn gundalow privateip rcarrillocruz trishnaguha kedarX
team_nso: cmoberg pekdon
team_nxos: GGabriele jedelman8 mikewiebe privateip rahushen rcarrillocruz trishnaguha kedarX
team_openstack: emonty j2sol juliakreger rcarrillocruz shrews thingee dagnello
team_openswitch: Qalthos gundalow privateip

View file

@ -36,6 +36,7 @@ The following is a list of module_utils files and a general description. The mod
- netcmd.py - Defines commands and comparison operators for use in networking modules
- netscaler.py - Utilities specifically for the netscaler network modules.
- network.py - Functions for running commands on networking devices
- nso.py - Utilities for modules that work with Cisco NSO.
- nxos.py - Contains definitions and helper functions specific to Cisco NXOS networking devices
- openstack.py - Utilities for modules that work with Openstack instances.
- openswitch.py - Definitions and helper functions for modules that manage OpenSwitch devices

View 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))

View file

@ -0,0 +1,276 @@
#!/usr/bin/python
# -*- 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 __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: nso_config
extends_documentation_fragment: nso
short_description: Manage Cisco NSO configuration and service synchronization.
description:
- This module provides support for managing configuration in Cisco NSO and
can also ensure services are in sync.
author: "Claes Nästén (@cnasten)"
options:
data:
description: >
NSO data in format as | display json converted to YAML. List entries can
be annotated with a __state entry. Set to in-sync/deep-in-sync for
services to verify service is in sync with the network. Set to absent in
list entries to ensure they are deleted if they exist in NSO.
required: true
version_added: "2.5"
'''
EXAMPLES = '''
- name: Create L3VPN
nso_config:
url: http://localhost:8080/jsonrpc
username: username
password: password
data:
l3vpn:vpn:
l3vpn:
- name: company
route-distinguisher: 999
endpoint:
- id: branch-office1
ce-device: ce6
ce-interface: GigabitEthernet0/12
ip-network: 10.10.1.0/24
bandwidth: 12000000
as-number: 65101
- id: branch-office2
ce-device: ce1
ce-interface: GigabitEthernet0/11
ip-network: 10.7.7.0/24
bandwidth: 6000000
as-number: 65102
- id: branch-office3
__state: absent
__state: in-sync
'''
RETURN = '''
changes:
description: List of changes
returned: always
type: complex
sample:
- path: "/l3vpn:vpn/l3vpn{example}/endpoint{office}/bandwidth"
from: '6000000'
to: '12000000'
type: set
contains:
path:
description: Path to value changed
returned: always
type: string
from:
description: Previous value if any, else null
returned: When previous value is present on value change
type: string
to:
description: Current value if any, else null.
returned: When new value is present on value change
type:
description: Type of change. create|delete|set|re-deploy
diffs:
description: List of sync changes
returned: always
type: complex
sample:
- path: "/l3vpn:vpn/l3vpn{example}"
diff: |2
devices {
device pe3 {
config {
alu:service {
vprn 65101 {
bgp {
group example-ce6 {
- peer-as 65102;
+ peer-as 65101;
}
}
}
}
}
}
}
contains:
path:
description: keypath to service changed
returned: always
type: string
diff:
description: configuration difference triggered the re-deploy
returned: always
type: string
'''
from ansible.module_utils.nso import connect, verify_version, nso_argument_spec
from ansible.module_utils.nso import State, ValueBuilder
from ansible.module_utils.nso import ModuleFailException, NsoException
from ansible.module_utils.basic import AnsibleModule
class NsoConfig(object):
def __init__(self, check_mode, client, data):
self._check_mode = check_mode
self._client = client
self._data = data
self._changes = []
self._diffs = []
def main(self):
# build list of values from configured data
value_builder = ValueBuilder(self._client)
for key, value in self._data.items():
value_builder.build('', key, value)
self._data_write(value_builder.values)
# check sync AFTER configuration is written
sync_values = self._sync_check(value_builder.values)
self._sync_ensure(sync_values)
return self._changes, self._diffs
def _data_write(self, values):
th = self._client.new_trans(mode='read_write')
for value in values:
if value.state == State.SET:
self._client.set_value(th, value.path, value.value)
elif value.state == State.PRESENT:
self._client.create(th, value.path)
elif value.state == State.ABSENT:
self._client.delete(th, value.path)
changes = self._client.get_trans_changes(th)
for change in changes:
if change['op'] == 'value_set':
self._changes.append({
'path': change['path'],
'from': change['old'] or None,
'to': change['value'],
'type': 'set'
})
elif change['op'] in ('created', 'deleted'):
self._changes.append({
'path': change['path'],
'type': change['op'][:-1]
})
if len(changes) > 0:
warnings = self._client.validate_commit(th)
if len(warnings) > 0:
raise NsoException(
'failed to validate transaction with warnings: {0}'.format(
', '.join((str(warning) for warning in warnings))), {})
if self._check_mode or len(changes) == 0:
self._client.delete_trans(th)
else:
self._client.commit(th)
def _sync_check(self, values):
sync_values = []
for value in values:
if value.state in (State.CHECK_SYNC, State.IN_SYNC):
action = 'check-sync'
elif value.state in (State.DEEP_CHECK_SYNC, State.DEEP_IN_SYNC):
action = 'deep-check-sync'
else:
action = None
if action is not None:
action_path = '{0}/{1}'.format(value.path, action)
action_params = {'outformat': 'cli'}
resp = self._client.run_action(None, action_path, action_params)
if len(resp) > 0:
sync_values.append(
ValueBuilder.Value(value.path, value.state, resp[0]['value']))
return sync_values
def _sync_ensure(self, sync_values):
for value in sync_values:
if value.state in (State.CHECK_SYNC, State.DEEP_CHECK_SYNC):
raise NsoException(
'{0} out of sync, diff {1}'.format(value.path, value.value), {})
action_path = '{0}/{1}'.format(value.path, 're-deploy')
if not self._check_mode:
result = self._client.run_action(None, action_path)
if not result:
raise NsoException(
'failed to re-deploy {0}'.format(value.path), {})
self._changes.append({'path': value.path, 'type': 're-deploy'})
self._diffs.append({'path': value.path, 'diff': value.value})
def main():
argument_spec = dict(
data=dict(required=True, type='dict')
)
argument_spec.update(nso_argument_spec)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
)
p = module.params
client = connect(p)
nso_config = NsoConfig(module.check_mode, client, p['data'])
try:
verify_version(client)
changes, diffs = nso_config.main()
client.logout()
changed = len(changes) > 0
module.exit_json(
changed=changed, changes=changes, diffs=diffs)
except NsoException as ex:
client.logout()
module.fail_json(msg=ex.message)
except ModuleFailException as ex:
client.logout()
module.fail_json(msg=ex.message)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,37 @@
# -*- 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/>.
#
class ModuleDocFragment(object):
DOCUMENTATION = '''
notes:
- Cisco NSO version 4.4.3 or higher required.
options:
url:
description: NSO JSON-RPC URL, http://localhost:8080/jsonrpc
required: true
username:
description: NSO username
required: true
password:
description: NSO password
required: true
'''

View file

@ -0,0 +1,245 @@
# 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 __future__ import (absolute_import, division, print_function)
import json
from ansible.compat.tests.mock import patch
from ansible.compat.tests import unittest
from ansible.module_utils import nso
MODULE_PREFIX_MAP = '''
{
"ansible-nso": "an",
"tailf-ncs": "ncs"
}
'''
SCHEMA_DATA = {
'/an:id-name-leaf': '''
{
"meta": {
"prefix": "an",
"namespace": "http://github.com/ansible/nso",
"types": {
"http://github.com/ansible/nso:id-name-t": [
{
"name": "http://github.com/ansible/nso:id-name-t",
"enumeration": [
{
"label": "id-one"
},
{
"label": "id-two"
}
]
},
{
"name": "identityref"
}
]
},
"keypath": "/an:id-name-leaf"
},
"data": {
"kind": "leaf",
"type": {
"namespace": "http://github.com/ansible/nso",
"name": "id-name-t"
},
"name": "id-name-leaf",
"qname": "an:id-name-leaf"
}
}''',
'/an:id-name-values': '''
{
"meta": {
"prefix": "an",
"namespace": "http://github.com/ansible/nso",
"types": {},
"keypath": "/an:id-name-values"
},
"data": {
"kind": "container",
"name": "id-name-values",
"qname": "an:id-name-values",
"children": [
{
"kind": "list",
"name": "id-name-value",
"qname": "an:id-name-value",
"key": [
"name"
]
}
]
}
}
''',
'/an:id-name-values/id-name-value': '''
{
"meta": {
"prefix": "an",
"namespace": "http://github.com/ansible/nso",
"types": {
"http://github.com/ansible/nso:id-name-t": [
{
"name": "http://github.com/ansible/nso:id-name-t",
"enumeration": [
{
"label": "id-one"
},
{
"label": "id-two"
}
]
},
{
"name": "identityref"
}
]
},
"keypath": "/an:id-name-values/id-name-value"
},
"data": {
"kind": "list",
"name": "id-name-value",
"qname": "an:id-name-value",
"key": [
"name"
],
"children": [
{
"kind": "key",
"name": "name",
"qname": "an:name",
"type": {
"namespace": "http://github.com/ansible/nso",
"name": "id-name-t"
}
},
{
"kind": "leaf",
"type": {
"primitive": true,
"name": "string"
},
"name": "value",
"qname": "an:value"
}
]
}
}
'''
}
class MockResponse(object):
def __init__(self, method, params, code, body, headers=None):
if headers is None:
headers = {}
self.method = method
self.params = params
self.code = code
self.body = body
self.headers = dict(headers)
def read(self):
return self.body
def mock_call(calls, url, data=None, headers=None, method=None):
result = calls[0]
del calls[0]
request = json.loads(data)
if result.method != request['method']:
raise ValueError('expected method {0}({1}), got {2}({3})'.format(
result.method, result.params,
request['method'], request['params']))
for key, value in result.params.items():
if key not in request['params']:
raise ValueError('{0} not in parameters'.format(key))
if value != request['params'][key]:
raise ValueError('expected {0} to be {1}, got {2}'.format(
key, value, request['params'][key]))
return result
def get_schema_response(path):
return MockResponse(
'get_schema', {'path': path}, 200, '{{"result": {0}}}'.format(
SCHEMA_DATA[path]))
class TestValueBuilder(unittest.TestCase):
@patch('ansible.module_utils.nso.open_url')
def test_identityref_leaf(self, open_url_mock):
calls = [
MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'),
get_schema_response('/an:id-name-leaf'),
MockResponse('get_module_prefix_map', {}, 200, '{{"result": {0}}}'.format(MODULE_PREFIX_MAP))
]
open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs)
parent = "/an:id-name-leaf"
schema_data = json.loads(
SCHEMA_DATA['/an:id-name-leaf'])
schema = schema_data['data']
vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc'))
vb.build(parent, None, 'ansible-nso:id-two', schema)
self.assertEquals(1, len(vb.values))
value = vb.values[0]
self.assertEquals(parent, value.path)
self.assertEquals('set', value.state)
self.assertEquals('an:id-two', value.value)
self.assertEqual(0, len(calls))
@patch('ansible.module_utils.nso.open_url')
def test_identityref_key(self, open_url_mock):
calls = [
MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'),
get_schema_response('/an:id-name-values/id-name-value'),
MockResponse('get_module_prefix_map', {}, 200, '{{"result": {0}}}'.format(MODULE_PREFIX_MAP)),
MockResponse('exists', {'path': '/an:id-name-values/id-name-value{an:id-one}'}, 200, '{"result": {"exists": true}}'),
]
open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs)
parent = "/an:id-name-values"
schema_data = json.loads(
SCHEMA_DATA['/an:id-name-values/id-name-value'])
schema = schema_data['data']
vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc'))
vb.build(parent, 'id-name-value', [{'name': 'ansible-nso:id-one', 'value': '1'}], schema)
self.assertEquals(1, len(vb.values))
value = vb.values[0]
self.assertEquals('{0}/id-name-value{{an:id-one}}/value'.format(parent), value.path)
self.assertEquals('set', value.state)
self.assertEquals('1', value.value)
self.assertEqual(0, len(calls))

View file

@ -0,0 +1,20 @@
{
"l3vpn:vpn": {
"l3vpn": [
{
"name": "company",
"route-distinguisher": 999,
"endpoint": [
{
"id": "branch-office1",
"ce-device": "ce6",
"ce-interface": "GigabitEthernet0/12",
"ip-network": "10.10.1.0/24",
"bandwidth": 12000000,
"as-number": 65101
}
]
}
]
}
}

View file

@ -0,0 +1,46 @@
{
"changes": [
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-device",
"old": "",
"value": "ce6",
"op": "value_set"
},
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ip-network",
"old": "",
"value": "10.10.1.0/24",
"op": "value_set"
},
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/as-number",
"old": "",
"value": "65101",
"op": "value_set"
},
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-interface",
"old": "",
"value": "GigabitEthernet0/12",
"op": "value_set"
},
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/bandwidth",
"old": "",
"value": "12000000",
"op": "value_set"
},
{
"path": "/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}",
"old": "",
"value": "",
"op": "created"
},
{
"path": "/l3vpn:vpn/l3vpn{company}",
"old": "",
"value": "",
"op": "modified"
}
]
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {}, "keypath": "/l3vpn:vpn/l3vpn/endpoint"}, "data": {"kind": "list", "leafref_groups": [["ce-device"]], "min_elements": 0, "name": "endpoint", "max_elements": "unbounded", "qname": "l3vpn:endpoint", "children": [{"info": {"string": "Endpoint identifier"}, "kind": "key", "mandatory": true, "name": "id", "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ce-device", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:ce-device", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/ncs:devices/device/name", "is_leafref": true}, {"kind": "leaf", "mandatory": true, "name": "ce-interface", "qname": "l3vpn:ce-interface", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "ip-network", "qname": "l3vpn:ip-network", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "ip-prefix"}}, {"info": {"string": "Bandwidth in bps"}, "kind": "leaf", "mandatory": true, "name": "bandwidth", "qname": "l3vpn:bandwidth", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"info": {"string": "CE Router as-number"}, "kind": "leaf", "name": "as-number", "qname": "l3vpn:as-number", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"primitive": true, "name": "uint32"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["id"], "mandatory": true, "leafrefGroups": [["ce-device"]]}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,131 @@
# 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 __future__ import (absolute_import, division, print_function)
import os
import json
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path not in fixture_data:
with open(path) as f:
data = json.load(f)
fixture_data[path] = data
return fixture_data[path]
class MockResponse(object):
def __init__(self, method, params, code, body, headers=None):
if headers is None:
headers = {}
self.method = method
self.params = params
self.code = code
self.body = body
self.headers = dict(headers)
def read(self):
return self.body
def mock_call(calls, url, data=None, headers=None, method=None):
result = calls[0]
del calls[0]
request = json.loads(data)
if result.method != request['method']:
raise ValueError('expected method {0}({1}), got {2}({3})'.format(
result.method, result.params,
request['method'], request['params']))
for key, value in result.params.items():
if key not in request['params']:
raise ValueError('{0} not in parameters'.format(key))
if value != request['params'][key]:
raise ValueError('expected {0} to be {1}, got {2}'.format(
key, value, request['params'][key]))
return result
class AnsibleExitJson(Exception):
pass
class AnsibleFailJson(Exception):
pass
class TestNsoModule(unittest.TestCase):
def execute_module(self, failed=False, changed=False, **kwargs):
if failed:
result = self.failed()
self.assertTrue(result['failed'], result)
else:
result = self.changed(changed)
self.assertEqual(result['changed'], changed, result)
for key, value in kwargs.items():
self.assertEqual(value, result[key])
return result
def failed(self):
def fail_json(*args, **kwargs):
kwargs['failed'] = True
raise AnsibleFailJson(kwargs)
with patch.object(basic.AnsibleModule, 'fail_json', fail_json):
with self.assertRaises(AnsibleFailJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertTrue(result['failed'], result)
return result
def changed(self, changed=False):
def exit_json(*args, **kwargs):
if 'changed' not in kwargs:
kwargs['changed'] = False
raise AnsibleExitJson(kwargs)
with patch.object(basic.AnsibleModule, 'exit_json', exit_json):
with self.assertRaises(AnsibleExitJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['changed'], changed, result)
return result

View file

@ -0,0 +1,134 @@
#
# 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 __future__ import (absolute_import, division, print_function)
import json
from ansible.compat.tests.mock import patch
from ansible.modules.network.nso import nso_config
from . import nso_module
from .nso_module import MockResponse
class TestNsoConfig(nso_module.TestNsoModule):
module = nso_config
@patch('ansible.module_utils.nso.open_url')
def test_nso_config_invalid_version_short(self, open_url_mock):
self._test_invalid_version(open_url_mock, '4.4')
@patch('ansible.module_utils.nso.open_url')
def test_nso_config_invalid_version_long(self, open_url_mock):
self._test_invalid_version(open_url_mock, '4.4.2')
def _test_invalid_version(self, open_url_mock, version):
calls = [
MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}),
MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "%s"}' % (version, )),
MockResponse('logout', {}, 200, '{"result": {}}'),
]
open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs)
data = nso_module.load_fixture('config_config.json')
nso_module.set_module_args({
'username': 'user', 'password': 'password',
'url': 'http://localhost:8080/jsonrpc', 'data': data
})
with self.assertRaises(SystemExit):
self.execute_module(changed=False, changes=[], diffs=[])
self.assertEqual(0, len(calls))
@patch('ansible.module_utils.nso.open_url')
def test_nso_config_valid_version_short(self, open_url_mock):
self._test_valid_version(open_url_mock, '4.5')
@patch('ansible.module_utils.nso.open_url')
def test_nso_config_valid_version_long(self, open_url_mock):
self._test_valid_version(open_url_mock, '4.4.3')
def _test_valid_version(self, open_url_mock, version):
calls = [
MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}),
MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "%s"}' % (version, )),
MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'),
MockResponse('get_trans_changes', {}, 200, '{"result": {"changes": []}}'),
MockResponse('delete_trans', {}, 200, '{"result": {}}'),
MockResponse('logout', {}, 200, '{"result": {}}'),
]
open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs)
data = nso_module.load_fixture('config_empty_data.json')
nso_module.set_module_args({
'username': 'user', 'password': 'password',
'url': 'http://localhost:8080/jsonrpc', 'data': data
})
self.execute_module(changed=False, changes=[], diffs=[])
self.assertEqual(0, len(calls))
@patch('ansible.module_utils.nso.open_url')
def test_nso_config_changed(self, open_url_mock):
vpn_schema = nso_module.load_fixture('l3vpn_schema.json')
l3vpn_schema = nso_module.load_fixture('l3vpn_l3vpn_schema.json')
endpoint_schema = nso_module.load_fixture('l3vpn_l3vpn_endpoint_schema.json')
changes = nso_module.load_fixture('config_config_changes.json')
calls = [
MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}),
MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.1"}'),
MockResponse('get_module_prefix_map', {}, 200, '{"result": {"l3vpn": "l3vpn"}}'),
MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'),
MockResponse('get_schema', {'path': '/l3vpn:vpn'}, 200, '{"result": %s}' % (json.dumps(vpn_schema, ))),
MockResponse('get_schema', {'path': '/l3vpn:vpn/l3vpn'}, 200, '{"result": %s}' % (json.dumps(l3vpn_schema, ))),
MockResponse('exists', {'path': '/l3vpn:vpn/l3vpn{company}'}, 200, '{"result": {"exists": true}}'),
MockResponse('get_schema', {'path': '/l3vpn:vpn/l3vpn/endpoint'}, 200, '{"result": %s}' % (json.dumps(endpoint_schema, ))),
MockResponse('exists', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}'}, 200, '{"result": {"exists": false}}'),
MockResponse('new_trans', {'mode': 'read_write'}, 200, '{"result": {"th": 2}}'),
MockResponse('create', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}'}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/route-distinguisher', 'value': 999}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/as-number', 'value': 65101}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/bandwidth', 'value': 12000000}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-device', 'value': 'ce6'}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-interface',
'value': 'GigabitEthernet0/12'}, 200, '{"result": {}}'),
MockResponse('set_value', {'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ip-network',
'value': '10.10.1.0/24'}, 200, '{"result": {}}'),
MockResponse('get_trans_changes', {}, 200, '{"result": %s}' % (json.dumps(changes), )),
MockResponse('validate_commit', {}, 200, '{"result": {}}'),
MockResponse('commit', {}, 200, '{"result": {}}'),
MockResponse('logout', {}, 200, '{"result": {}}'),
]
open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs)
data = nso_module.load_fixture('config_config.json')
nso_module.set_module_args({
'username': 'user', 'password': 'password',
'url': 'http://localhost:8080/jsonrpc', 'data': data
})
self.execute_module(changed=True, changes=[
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-device', 'type': 'set', 'from': None, 'to': 'ce6'},
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ip-network', 'type': 'set', 'from': None, 'to': '10.10.1.0/24'},
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/as-number', 'type': 'set', 'from': None, 'to': '65101'},
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/ce-interface', 'type': 'set', 'from': None, 'to': 'GigabitEthernet0/12'},
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}/bandwidth', 'type': 'set', 'from': None, 'to': '12000000'},
{'path': '/l3vpn:vpn/l3vpn{company}/endpoint{branch-office1}', 'type': 'create'},
], diffs=[])
self.assertEqual(0, len(calls))