diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index 8ea245fc08..24bd11bd97 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -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
diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst
index 21bc9ede36..32ba57ca82 100644
--- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst
+++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst
@@ -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
diff --git a/lib/ansible/module_utils/nso.py b/lib/ansible/module_utils/nso.py
new file mode 100644
index 0000000000..711c3bb6d5
--- /dev/null
+++ b/lib/ansible/module_utils/nso.py
@@ -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 .
+#
+
+
+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'.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))
diff --git a/lib/ansible/modules/network/nso/__init__.py b/lib/ansible/modules/network/nso/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/ansible/modules/network/nso/nso_config.py b/lib/ansible/modules/network/nso/nso_config.py
new file mode 100644
index 0000000000..f60ceaf8bb
--- /dev/null
+++ b/lib/ansible/modules/network/nso/nso_config.py
@@ -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 .
+#
+
+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()
diff --git a/lib/ansible/utils/module_docs_fragments/nso.py b/lib/ansible/utils/module_docs_fragments/nso.py
new file mode 100644
index 0000000000..c262c133d6
--- /dev/null
+++ b/lib/ansible/utils/module_docs_fragments/nso.py
@@ -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 .
+#
+
+
+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
+'''
diff --git a/test/units/module_utils/test_nso.py b/test/units/module_utils/test_nso.py
new file mode 100644
index 0000000000..7b7c74a6f4
--- /dev/null
+++ b/test/units/module_utils/test_nso.py
@@ -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 .
+#
+
+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))
diff --git a/test/units/modules/network/nso/__init__.py b/test/units/modules/network/nso/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/units/modules/network/nso/fixtures/config_config.json b/test/units/modules/network/nso/fixtures/config_config.json
new file mode 100644
index 0000000000..b7318586b5
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/config_config.json
@@ -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
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/test/units/modules/network/nso/fixtures/config_config_changes.json b/test/units/modules/network/nso/fixtures/config_config_changes.json
new file mode 100644
index 0000000000..3ef234b7ff
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/config_config_changes.json
@@ -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"
+ }
+ ]
+}
diff --git a/test/units/modules/network/nso/fixtures/config_empty_data.json b/test/units/modules/network/nso/fixtures/config_empty_data.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/config_empty_data.json
@@ -0,0 +1 @@
+{}
diff --git a/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_endpoint_schema.json b/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_endpoint_schema.json
new file mode 100644
index 0000000000..0330aeb9b9
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_endpoint_schema.json
@@ -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"]]}}
diff --git a/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_schema.json b/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_schema.json
new file mode 100644
index 0000000000..2737e7a547
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/l3vpn_l3vpn_schema.json
@@ -0,0 +1 @@
+{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {"http://com/example/l3vpn:t19": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t19"}], "leaf_type": [{"name": "instance-identifier"}]}], "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number": [{"range": {"value": [["0", "65535"]]}, "name": "urn:ietf:params:xml:ns:yang:ietf-inet-types:port-number"}, {"name": "uint16"}], "http://tail-f.com/ns/ncs:outformat-deep-check-sync": [{"name": "http://tail-f.com/ns/ncs:outformat-deep-check-sync", "enumeration": [{"info": "The CLI config that would have to be applied\nto the device(s) in order for the service to\nbecome in sync with the network.", "label": "cli"}, {"info": "The XML (NETCONF format) that would have to be\napplied to the device(s) in order for the service to\nbecome in sync with the network.", "label": "xml"}, {"info": "Returns if the service is in sync or not.", "label": "boolean"}]}, {"name": "string"}], "http://com/example/l3vpn:t21": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t21"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t15": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t15"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:protocol-type": [{"name": "http://com/example/l3vpn:protocol-type", "enumeration": [{"label": "icmp"}, {"label": "igmp"}, {"label": "ipip"}, {"label": "tcp"}, {"label": "egp"}, {"label": "udp"}, {"label": "rsvp"}, {"label": "gre"}, {"label": "esp"}, {"label": "ah"}, {"label": "icmp6"}, {"label": "ospf"}, {"label": "pim"}, {"label": "sctp"}]}, {"name": "string"}], "http://com/example/l3vpn:t17": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t17"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t23": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t23"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t11": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t11"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t13": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t13"}], "leaf_type": [{"name": "instance-identifier"}]}], "http://com/example/l3vpn:t24": [{"name": "http://com/example/l3vpn:t24", "enumeration": [{"label": "waiting"}, {"label": "executing"}, {"label": "blocking"}, {"label": "blocked"}, {"label": "failed"}, {"label": "admin-cleared"}, {"label": "commit-queue-failed"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:log-entry-t": [{"info": "This leaf identifies the specific log entry.", "name": "http://tail-f.com/ns/ncs:log-entry-t", "enumeration": [{"label": "device-modified"}, {"label": "service-modified"}]}, {"name": "identityref"}], "http://com/example/l3vpn:t7": [{"name": "http://com/example/l3vpn:t7", "enumeration": [{"label": "async"}, {"label": "timeout"}, {"label": "deleted"}]}, {"name": "string"}], "http://com/example/l3vpn:qos-match-type": [{"union": [[{"name": "ipv4-address-and-prefix-length"}], [{"name": "http://com/example/l3vpn:t2", "enumeration": [{"label": "any"}]}, {"name": "string"}]]}], "http://com/example/l3vpn:t9": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t9"}], "leaf_type": [{"name": "string"}]}], "http://tail-f.com/ns/ncs:outformat4": [{"name": "http://tail-f.com/ns/ncs:outformat4", "enumeration": [{"info": "NCS CLI curly bracket format.", "label": "cli"}, {"info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", "label": "xml"}, {"info": "The actual data in native format that would be sent to\nthe device", "label": "native"}, {"label": "boolean"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:log-entry-level-t": [{"info": "Levels used for identifying the severity of an event.\nLevels are organized from least specific to most where\n'all' is least specific and 'error' is most specific.", "name": "http://tail-f.com/ns/ncs:log-entry-level-t", "enumeration": [{"label": "all"}, {"label": "trace"}, {"label": "debug"}, {"label": "info"}, {"label": "warn"}, {"label": "error"}]}, {"name": "string"}], "http://tail-f.com/ns/ncs:outformat2": [{"name": "http://tail-f.com/ns/ncs:outformat2", "enumeration": [{"info": "NCS CLI curly bracket format.", "label": "cli"}, {"info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", "label": "xml"}]}, {"name": "string"}]}, "keypath": "/l3vpn:vpn/l3vpn"}, "data": {"kind": "list", "leafref_groups": [["used-by-customer-service"]], "min_elements": 0, "name": "l3vpn", "max_elements": "unbounded", "qname": "l3vpn:l3vpn", "children": [{"info": {"string": "Unique service id"}, "kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Devices and other services this service modified directly or\nindirectly."}, "kind": "container", "mandatory": true, "name": "modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false, "children": [{"info": {"string": "Devices this service modified directly or indirectly"}, "kind": "leaf-list", "name": "devices", "is_leafref": true, "qname": "l3vpn:devices", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"namespace": "http://com/example/l3vpn", "name": "t9"}, "config": false}, {"info": {"string": "Services this service modified directly or indirectly"}, "kind": "leaf-list", "name": "services", "type": {"namespace": "http://com/example/l3vpn", "name": "t11"}, "qname": "l3vpn:services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Services residing on remote LSA nodes this service\nhas modified directly or indirectly."}, "kind": "leaf-list", "name": "lsa-services", "type": {"namespace": "http://com/example/l3vpn", "name": "t13"}, "qname": "l3vpn:lsa-services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}, {"info": {"string": "Devices and other services this service has explicitly\nmodified."}, "kind": "container", "mandatory": true, "name": "directly-modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:directly-modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false, "children": [{"info": {"string": "Devices this service has explicitly modified."}, "kind": "leaf-list", "name": "devices", "is_leafref": true, "qname": "l3vpn:devices", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"namespace": "http://com/example/l3vpn", "name": "t15"}, "config": false}, {"info": {"string": "Services this service has explicitly modified."}, "kind": "leaf-list", "name": "services", "type": {"namespace": "http://com/example/l3vpn", "name": "t17"}, "qname": "l3vpn:services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Services residing on remote LSA nodes this service\nhas explicitly modified."}, "kind": "leaf-list", "name": "lsa-services", "type": {"namespace": "http://com/example/l3vpn", "name": "t19"}, "qname": "l3vpn:lsa-services", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}]}, {"info": {"string": "A list of devices this service instance has manipulated"}, "kind": "leaf-list", "name": "device-list", "type": {"namespace": "http://com/example/l3vpn", "name": "t21"}, "qname": "l3vpn:device-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Customer facing services using this service"}, "kind": "leaf-list", "name": "used-by-customer-service", "is_leafref": true, "qname": "l3vpn:used-by-customer-service", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:services/customer-service/object-id", "type": {"namespace": "http://com/example/l3vpn", "name": "t23"}, "config": false}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false, "children": [{"kind": "list", "min_elements": 0, "name": "queue-item", "max_elements": "unbounded", "qname": "l3vpn:queue-item", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["id"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "status", "type": {"namespace": "http://com/example/l3vpn", "name": "t24"}, "qname": "l3vpn:status", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"kind": "list", "leafref_groups": [["name"]], "min_elements": 0, "name": "failed-device", "max_elements": "unbounded", "qname": "l3vpn:failed-device", "children": [{"kind": "key", "mandatory": true, "name": "name", "is_leafref": true, "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_target": "/ncs:devices/device/name", "type": {"primitive": true, "name": "string"}, "config": false}, {"kind": "leaf", "name": "time", "config": false, "qname": "l3vpn:time", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "date-and-time"}}, {"kind": "leaf", "name": "config-data", "is_cli_preformatted": true, "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:config-data", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"kind": "leaf", "name": "error", "config": false, "qname": "l3vpn:error", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["name"], "mandatory": true, "config": false, "leafrefGroups": [["name"]]}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "admin-clear", "qname": "l3vpn:admin-clear"}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "delete", "qname": "l3vpn:delete"}]}, {"access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "kind": "action", "mandatory": true, "name": "clear", "qname": "l3vpn:clear"}]}, {"kind": "container", "mandatory": true, "name": "log", "qname": "l3vpn:log", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false, "children": [{"kind": "list", "min_elements": 0, "name": "log-entry", "max_elements": "unbounded", "qname": "l3vpn:log-entry", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "key": ["when"], "mandatory": true, "config": false, "children": [{"kind": "key", "mandatory": true, "name": "when", "type": {"primitive": true, "name": "date-and-time"}, "qname": "l3vpn:when", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "type", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "log-entry-t"}, "qname": "l3vpn:type", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "level", "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "log-entry-level-t"}, "qname": "l3vpn:level", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "name": "message", "config": false, "qname": "l3vpn:message", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "type": {"primitive": true, "name": "string"}}]}, {"info": {"string": "Remove log entries"}, "kind": "action", "mandatory": true, "name": "purge", "qname": "l3vpn:purge", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}]}, {"kind": "leaf", "mandatory": true, "name": "route-distinguisher", "qname": "l3vpn:route-distinguisher", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"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"]]}, {"kind": "container", "mandatory": true, "name": "qos", "leafrefGroups": [["qos-policy"]], "qname": "l3vpn:qos", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_groups": [["qos-policy"]], "children": [{"kind": "leaf", "name": "qos-policy", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:qos-policy", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "leafref_target": "/l3vpn:qos/qos-policy/name", "is_leafref": true}, {"kind": "list", "leafref_groups": [["qos-class"]], "min_elements": 0, "name": "custom-qos-match", "max_elements": "unbounded", "qname": "l3vpn:custom-qos-match", "children": [{"kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"kind": "leaf", "mandatory": true, "name": "qos-class", "type": {"primitive": true, "name": "string"}, "qname": "l3vpn:qos-class", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_target": "/l3vpn:qos/qos-class/name", "is_leafref": true}, {"access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "kind": "leaf", "type": {"namespace": "http://com/example/l3vpn", "name": "qos-match-type"}, "name": "source-ip", "qname": "l3vpn:source-ip"}, {"access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "kind": "leaf", "type": {"namespace": "http://com/example/l3vpn", "name": "qos-match-type"}, "name": "destination-ip", "qname": "l3vpn:destination-ip"}, {"info": {"string": "Destination IP port"}, "kind": "leaf", "name": "port-start", "qname": "l3vpn:port-start", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Destination IP port"}, "kind": "leaf", "name": "port-end", "qname": "l3vpn:port-end", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "urn:ietf:params:xml:ns:yang:ietf-inet-types", "name": "port-number"}}, {"info": {"string": "Source IP protocol"}, "kind": "leaf", "name": "protocol", "qname": "l3vpn:protocol", "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "type": {"namespace": "http://com/example/l3vpn", "name": "protocol-type"}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["qos-class"]]}]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "l3vpn:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "default": "boolean", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat4"}}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"info": {"string": "Return list only contains negatives"}, "kind": "leaf", "is_action_input": true, "name": "suppress-positive-result", "qname": "l3vpn:suppress-positive-result", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "in-sync", "children": [{"kind": "leaf", "name": "in-sync", "qname": "l3vpn:in-sync", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "boolean"}, "is_action_output": true}]}, {"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "deep-check-sync", "qname": "l3vpn:deep-check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "default": "boolean", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat-deep-check-sync"}}, {"info": {"string": "Return list only contains negatives"}, "kind": "leaf", "is_action_input": true, "name": "suppress-positive-result", "qname": "l3vpn:suppress-positive-result", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-sync", "children": [{"kind": "container", "mandatory": true, "name": "sync-result", "qname": "l3vpn:sync-result", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Run/Dryrun the service logic again"}, "kind": "action", "mandatory": true, "name": "re-deploy", "qname": "l3vpn:re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "container", "is_action_input": true, "name": "dry-run", "presence": true, "qname": "l3vpn:dry-run", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "is_action_input": true, "name": "no-revision-drop", "qname": "l3vpn:no-revision-drop", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "leaf", "is_action_input": true, "name": "no-networking", "qname": "l3vpn:no-networking", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "no-overwrite", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-overwrite", "qname": "l3vpn:no-overwrite", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-out-of-sync-check", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-out-of-sync-check", "qname": "l3vpn:no-out-of-sync-check", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-sync-check"}, {"kind": "container", "is_action_input": true, "name": "commit-queue", "presence": true, "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"kind": "container", "is_action_input": true, "name": "reconcile", "presence": true, "qname": "l3vpn:reconcile", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}, {"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}, {"info": {"string": "Reactive redeploy of service logic"}, "kind": "action", "mandatory": true, "name": "reactive-re-deploy", "qname": "l3vpn:reactive-re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}, {"info": {"string": "Touch a service"}, "kind": "action", "mandatory": true, "name": "touch", "qname": "l3vpn:touch", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": []}, {"info": {"string": "Get the data this service created"}, "kind": "action", "mandatory": true, "name": "get-modifications", "qname": "l3vpn:get-modifications", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "leaf", "is_action_input": true, "name": "outformat", "qname": "l3vpn:outformat", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"namespace": "http://tail-f.com/ns/ncs", "name": "outformat2"}}, {"kind": "leaf", "is_action_input": true, "name": "reverse", "qname": "l3vpn:reverse", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"default": "deep", "kind": "choice", "cases": [{"kind": "case", "name": "deep", "children": [{"kind": "leaf", "is_action_input": true, "name": "deep", "qname": "l3vpn:deep", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "shallow", "children": [{"kind": "leaf", "is_action_input": true, "name": "shallow", "qname": "l3vpn:shallow", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "depth"}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}]}, {"info": {"string": "Undo the effects of this service"}, "kind": "action", "mandatory": true, "name": "un-deploy", "qname": "l3vpn:un-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}, "children": [{"kind": "container", "is_action_input": true, "name": "dry-run", "presence": true, "qname": "l3vpn:dry-run", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "leaf", "is_action_input": true, "name": "no-revision-drop", "qname": "l3vpn:no-revision-drop", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "leaf", "is_action_input": true, "name": "no-networking", "qname": "l3vpn:no-networking", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "no-overwrite", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-overwrite", "qname": "l3vpn:no-overwrite", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-out-of-sync-check", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-out-of-sync-check", "qname": "l3vpn:no-out-of-sync-check", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-sync-check"}, {"kind": "container", "is_action_input": true, "name": "commit-queue", "presence": true, "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}}, {"kind": "choice", "cases": [{"kind": "case", "name": "use-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "use-lsa", "qname": "l3vpn:use-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}, {"kind": "case", "name": "no-lsa", "children": [{"kind": "leaf", "is_action_input": true, "name": "no-lsa", "qname": "l3vpn:no-lsa", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}]}], "name": "choice-lsa"}, {"kind": "leaf", "is_action_input": true, "name": "ignore-refcount", "qname": "l3vpn:ignore-refcount", "access": {"read": false, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "empty"}}, {"kind": "choice", "cases": [{"kind": "case", "name": "case-xml", "children": [{"kind": "container", "mandatory": true, "name": "result-xml", "qname": "l3vpn:result-xml", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-cli", "children": [{"kind": "container", "mandatory": true, "name": "cli", "qname": "l3vpn:cli", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}, {"kind": "case", "name": "case-native", "children": [{"kind": "container", "mandatory": true, "name": "native", "qname": "l3vpn:native", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}]}], "name": "outformat"}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "is_action_output": true}, {"kind": "leaf", "name": "id", "type": {"primitive": true, "name": "uint64"}, "qname": "l3vpn:id", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "leafref_target": "/ncs:devices/commit-queue/queue-item/id", "is_leafref": true, "is_action_output": true}, {"kind": "leaf", "name": "tag", "qname": "l3vpn:tag", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"primitive": true, "name": "string"}, "is_action_output": true}, {"kind": "leaf", "name": "status", "qname": "l3vpn:status", "access": {"read": false, "create": false, "execute": false, "update": false, "delete": false}, "type": {"namespace": "http://com/example/l3vpn", "name": "t7"}, "is_action_output": true}]}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["used-by-customer-service"]]}}
diff --git a/test/units/modules/network/nso/fixtures/l3vpn_schema.json b/test/units/modules/network/nso/fixtures/l3vpn_schema.json
new file mode 100644
index 0000000000..0e7e370382
--- /dev/null
+++ b/test/units/modules/network/nso/fixtures/l3vpn_schema.json
@@ -0,0 +1 @@
+{"meta": {"prefix": "l3vpn", "namespace": "http://com/example/l3vpn", "types": {"http://com/example/l3vpn:t21": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t21"}], "leaf_type": [{"name": "string"}]}], "http://com/example/l3vpn:t23": [{"list_type": [{"leaf-list": true, "name": "http://com/example/l3vpn:t23"}], "leaf_type": [{"name": "string"}]}]}, "keypath": "/l3vpn:vpn"}, "data": {"kind": "container", "mandatory": true, "name": "vpn", "qname": "l3vpn:vpn", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "children": [{"kind": "list", "leafref_groups": [["used-by-customer-service"]], "min_elements": 0, "name": "l3vpn", "max_elements": "unbounded", "qname": "l3vpn:l3vpn", "children": [{"info": {"string": "Unique service id"}, "kind": "key", "mandatory": true, "name": "name", "qname": "l3vpn:name", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "string"}}, {"info": {"string": "Devices and other services this service modified directly or\nindirectly."}, "kind": "container", "mandatory": true, "name": "modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false}, {"info": {"string": "Devices and other services this service has explicitly\nmodified."}, "kind": "container", "mandatory": true, "name": "directly-modified", "leafrefGroups": [["devices"]], "qname": "l3vpn:directly-modified", "is_config_false_callpoint": true, "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "leafref_groups": [["devices"]], "config": false}, {"info": {"string": "A list of devices this service instance has manipulated"}, "kind": "leaf-list", "name": "device-list", "type": {"namespace": "http://com/example/l3vpn", "name": "t21"}, "qname": "l3vpn:device-list", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "config": false}, {"info": {"string": "Customer facing services using this service"}, "kind": "leaf-list", "name": "used-by-customer-service", "is_leafref": true, "qname": "l3vpn:used-by-customer-service", "is_config_false_callpoint": true, "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "readonly": true, "leafref_target": "/ncs:services/customer-service/object-id", "type": {"namespace": "http://com/example/l3vpn", "name": "t23"}, "config": false}, {"kind": "container", "mandatory": true, "name": "commit-queue", "qname": "l3vpn:commit-queue", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "container", "mandatory": true, "name": "log", "qname": "l3vpn:log", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "readonly": true, "config": false}, {"kind": "leaf", "mandatory": true, "name": "route-distinguisher", "qname": "l3vpn:route-distinguisher", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "type": {"primitive": true, "name": "uint32"}}, {"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"]]}, {"kind": "container", "mandatory": true, "name": "qos", "leafrefGroups": [["qos-policy"]], "qname": "l3vpn:qos", "access": {"read": true, "create": false, "execute": false, "update": true, "delete": false}, "leafref_groups": [["qos-policy"]]}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "check-sync", "qname": "l3vpn:check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Check if device config is according to the service"}, "kind": "action", "mandatory": true, "name": "deep-check-sync", "qname": "l3vpn:deep-check-sync", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Run/Dryrun the service logic again"}, "kind": "action", "mandatory": true, "name": "re-deploy", "qname": "l3vpn:re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Reactive redeploy of service logic"}, "kind": "action", "mandatory": true, "name": "reactive-re-deploy", "qname": "l3vpn:reactive-re-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Touch a service"}, "kind": "action", "mandatory": true, "name": "touch", "qname": "l3vpn:touch", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Get the data this service created"}, "kind": "action", "mandatory": true, "name": "get-modifications", "qname": "l3vpn:get-modifications", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}, {"info": {"string": "Undo the effects of this service"}, "kind": "action", "mandatory": true, "name": "un-deploy", "qname": "l3vpn:un-deploy", "access": {"read": false, "create": false, "execute": true, "update": false, "delete": false}}], "access": {"read": true, "create": true, "execute": false, "update": true, "delete": true}, "key": ["name"], "mandatory": true, "leafrefGroups": [["used-by-customer-service"]]}]}}
diff --git a/test/units/modules/network/nso/nso_module.py b/test/units/modules/network/nso/nso_module.py
new file mode 100644
index 0000000000..44ce5d5049
--- /dev/null
+++ b/test/units/modules/network/nso/nso_module.py
@@ -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 .
+
+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
diff --git a/test/units/modules/network/nso/test_nso_config.py b/test/units/modules/network/nso/test_nso_config.py
new file mode 100644
index 0000000000..546688a0ce
--- /dev/null
+++ b/test/units/modules/network/nso/test_nso_config.py
@@ -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 .
+
+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))