Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nso_config work around ordering issues #36774

Merged
merged 1 commit into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 55 additions & 16 deletions lib/ansible/module_utils/network/nso/nso.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,17 @@ def _get_th(self, mode='read'):


class ValueBuilder(object):
PATH_RE = re.compile('{[^}]*}')

class Value(object):
__slots__ = ['path', 'state', 'value']
__slots__ = ['path', 'tag_path', 'state', 'value', 'deps']

def __init__(self, path, state, value):
def __init__(self, path, state, value, deps):
self.path = path
self.tag_path = ValueBuilder.PATH_RE.sub('', path)
self.state = state
self.value = value
self.deps = deps

def __lt__(self, rhs):
l_len = len(self.path.split('/'))
Expand All @@ -298,7 +302,6 @@ def __init__(self, client):
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)
Expand All @@ -313,25 +316,26 @@ def build(self, parent, maybe_qname, value, schema=None):
if self._is_leaf_list(schema) and is_version(self._client, [(4, 5)]):
self._build_leaf_list(path, schema, value)
elif self._is_leaf(schema):
deps = schema.get('deps', [])
if self._is_empty_leaf(schema):
exists = self._client.exists(path)
if exists and value != [None]:
self._add_value(path, State.ABSENT, None)
self._add_value(path, State.ABSENT, None, deps)
elif not exists and value == [None]:
self._add_value(path, State.PRESENT, None)
self._add_value(path, State.PRESENT, None, deps)
else:
if maybe_qname is None:
value_type = self._get_type(path)
else:
value_type = self._get_child_type(parent, maybe_qname)
value_type = self._get_child_type(parent, qname)

if 'identityref' in value_type:
if isinstance(value, list):
value = [ll_v for ll_v, t_ll_v
in [self._get_prefix_name(v) for v in value]]
else:
value, t_value = self._get_prefix_name(value)
self._add_value(path, State.SET, value)
self._add_value(path, State.SET, value, deps)
elif isinstance(value, dict):
self._build_dict(path, schema, value)
elif isinstance(value, list):
Expand All @@ -344,11 +348,44 @@ def build(self, parent, maybe_qname, value, schema=None):
@property
def values(self):
if self._values_dirty:
self._values.sort()
self._values = self._sort_values(sorted(self._values))
self._values_dirty = False

return self._values

def _sort_values(self, values):
class N(object):
def __init__(self, v):
self.tmp_mark = False
self.mark = False
self.v = v

sorted_values = []
nodes = [N(v) for v in values]

def visit(n):
if n.tmp_mark:
return False
if not n.mark:
n.tmp_mark = True
for m in nodes:
if m.v.tag_path in n.v.deps:
if not visit(m):
return False

n.tmp_mark = False
n.mark = True

sorted_values.insert(0, n.v)

return True

while any((not n.mark for n in nodes)):
n = next((node for node in nodes if not node.mark))
visit(n)

return sorted_values[::-1]

def _build_dict(self, path, schema, value):
keys = schema.get('key', [])
for dict_key, dict_value in value.items():
Expand All @@ -360,18 +397,20 @@ def _build_dict(self, path, schema, value):
self.build(path, dict_key, dict_value, child_schema)

def _build_leaf_list(self, path, schema, value):
deps = schema.get('deps', [])
entry_type = self._get_type(path, schema)
# remove leaf list if treated as a list and then re-create the
# expected list entries.
self._add_value(path, State.ABSENT, None)
self._add_value(path, State.ABSENT, None, deps)

for entry in value:
if 'identityref' in entry_type:
entry, t_entry = self._get_prefix_name(entry)
entry_path = '{0}{{{1}}}'.format(path, entry)
self._add_value(entry_path, State.PRESENT, None)
self._add_value(entry_path, State.PRESENT, None, deps)

def _build_list(self, path, schema, value):
deps = schema.get('deps', [])
for entry in value:
entry_key = self._build_key(path, entry, schema['key'])
entry_path = '{0}{{{1}}}'.format(path, entry_key)
Expand All @@ -380,12 +419,12 @@ def _build_list(self, path, schema, value):

if entry_state == 'absent':
if entry_exists:
self._add_value(entry_path, State.ABSENT, None)
self._add_value(entry_path, State.ABSENT, None, deps)
else:
if not entry_exists:
self._add_value(entry_path, State.PRESENT, None)
self._add_value(entry_path, State.PRESENT, None, deps)
if entry_state in State.SYNC_STATES:
self._add_value(entry_path, entry_state, None)
self._add_value(entry_path, entry_state, None, deps)

self.build(entry_path, None, entry)

Expand Down Expand Up @@ -439,8 +478,8 @@ def _find_child(self, path, schema, qname):
'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))
def _add_value(self, path, state, value, deps):
self._values.append(ValueBuilder.Value(path, state, value, deps))
self._values_dirty = True

def _get_prefix_name(self, qname):
Expand Down Expand Up @@ -496,7 +535,7 @@ def get_type(meta, curr_type):
return None

def _ensure_schema_cached(self, path):
path = self._path_re.sub('', path)
path = ValueBuilder.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
Expand Down
106 changes: 106 additions & 0 deletions test/units/module_utils/network/nso/test_nso.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,48 @@
}
}
}
''',
'/test:deps': '''
{
"meta": {
},
"data": {
"kind":"container",
"name":"deps",
"qname":"test:deps",
"children": [
{
"kind": "leaf",
"type": {
"primitive": true,
"name": "string"
},
"name": "a",
"qname": "test:a",
"deps": ["/test:deps/c"]
},
{
"kind": "leaf",
"type": {
"primitive": true,
"name": "string"
},
"name": "b",
"qname": "test:b",
"deps": ["/test:deps/a"]
},
{
"kind": "leaf",
"type": {
"primitive": true,
"name": "string"
},
"name": "c",
"qname": "test:c"
}
]
}
}
'''
}

Expand Down Expand Up @@ -476,6 +518,70 @@ def test_leaf_list_type_45(self, open_url_mock):

self.assertEqual(0, len(calls))

@patch('ansible.module_utils.network.nso.nso.open_url')
def test_sort_by_deps(self, open_url_mock):
calls = [
MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'),
get_schema_response('/test:deps')
]
open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs)

parent = "/test:deps"
schema_data = json.loads(
SCHEMA_DATA['/test:deps'])
schema = schema_data['data']

values = {
'a': '1',
'b': '2',
'c': '3',
}

vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc'))
vb.build(parent, None, values, schema)
self.assertEquals(3, len(vb.values))
value = vb.values[0]
self.assertEquals('{0}/c'.format(parent), value.path)
self.assertEquals('3', value.value)
value = vb.values[1]
self.assertEquals('{0}/a'.format(parent), value.path)
self.assertEquals('1', value.value)
value = vb.values[2]
self.assertEquals('{0}/b'.format(parent), value.path)
self.assertEquals('2', value.value)

self.assertEqual(0, len(calls))

@patch('ansible.module_utils.network.nso.nso.open_url')
def test_sort_by_deps_not_included(self, open_url_mock):
calls = [
MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'),
get_schema_response('/test:deps')
]
open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs)

parent = "/test:deps"
schema_data = json.loads(
SCHEMA_DATA['/test:deps'])
schema = schema_data['data']

values = {
'a': '1',
'b': '2'
}

vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc'))
vb.build(parent, None, values, schema)
self.assertEquals(2, len(vb.values))
value = vb.values[0]
self.assertEquals('{0}/a'.format(parent), value.path)
self.assertEquals('1', value.value)
value = vb.values[1]
self.assertEquals('{0}/b'.format(parent), value.path)
self.assertEquals('2', value.value)

self.assertEqual(0, len(calls))


class TestVerifyVersion(unittest.TestCase):
def test_valid_versions(self):
Expand Down