1# -*- coding: utf-8 -*-
2
3# Copyright: (c) 2017, Cisco and/or its affiliates.
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from ansible.module_utils.basic import env_fallback
7from ansible.module_utils.urls import open_url
8from ansible.module_utils._text import to_text
9
10import json
11import re
12import socket
13
14try:
15    unicode
16    HAVE_UNICODE = True
17except NameError:
18    unicode = str
19    HAVE_UNICODE = False
20
21
22nso_argument_spec = dict(
23    url=dict(type='str', required=True),
24    username=dict(type='str', required=True, fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
25    password=dict(type='str', required=True, no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
26    timeout=dict(type='int', default=300),
27    validate_certs=dict(type='bool', default=False)
28)
29
30
31class State(object):
32    SET = 'set'
33    PRESENT = 'present'
34    ABSENT = 'absent'
35    CHECK_SYNC = 'check-sync'
36    DEEP_CHECK_SYNC = 'deep-check-sync'
37    IN_SYNC = 'in-sync'
38    DEEP_IN_SYNC = 'deep-in-sync'
39
40    SYNC_STATES = ('check-sync', 'deep-check-sync', 'in-sync', 'deep-in-sync')
41
42
43class ModuleFailException(Exception):
44    def __init__(self, message):
45        super(ModuleFailException, self).__init__(message)
46        self.message = message
47
48
49class NsoException(Exception):
50    def __init__(self, message, error):
51        super(NsoException, self).__init__(message)
52        self.message = message
53        self.error = error
54
55
56class JsonRpc(object):
57    def __init__(self, url, timeout, validate_certs):
58        self._url = url
59        self._timeout = timeout
60        self._validate_certs = validate_certs
61        self._id = 0
62        self._trans = {}
63        self._headers = {'Content-Type': 'application/json'}
64        self._conn = None
65        self._system_settings = {}
66
67    def login(self, user, passwd):
68        payload = {
69            'method': 'login',
70            'params': {'user': user, 'passwd': passwd}
71        }
72        resp, resp_json = self._call(payload)
73        self._headers['Cookie'] = resp.headers['set-cookie']
74
75    def logout(self):
76        payload = {'method': 'logout', 'params': {}}
77        self._call(payload)
78
79    def get_system_setting(self, setting):
80        if setting not in self._system_settings:
81            payload = {'method': 'get_system_setting', 'params': {'operation': setting}}
82            resp, resp_json = self._call(payload)
83            self._system_settings[setting] = resp_json['result']
84        return self._system_settings[setting]
85
86    def new_trans(self, **kwargs):
87        payload = {'method': 'new_trans', 'params': kwargs}
88        resp, resp_json = self._call(payload)
89        return resp_json['result']['th']
90
91    def get_trans(self, mode):
92        if mode not in self._trans:
93            th = self.new_trans(mode=mode)
94            self._trans[mode] = th
95        return self._trans[mode]
96
97    def delete_trans(self, th):
98        payload = {'method': 'delete_trans', 'params': {'th': th}}
99        resp, resp_json = self._call(payload)
100        self._maybe_delete_trans(th)
101
102    def validate_trans(self, th):
103        payload = {'method': 'validate_trans', 'params': {'th': th}}
104        resp, resp_json = self._write_call(payload)
105        return resp_json['result']
106
107    def get_trans_changes(self, th):
108        payload = {'method': 'get_trans_changes', 'params': {'th': th}}
109        resp, resp_json = self._write_call(payload)
110        return resp_json['result']['changes']
111
112    def validate_commit(self, th):
113        payload = {'method': 'validate_commit', 'params': {'th': th}}
114        resp, resp_json = self._write_call(payload)
115        return resp_json['result'].get('warnings', [])
116
117    def commit(self, th):
118        payload = {'method': 'commit', 'params': {'th': th}}
119        resp, resp_json = self._write_call(payload)
120        if len(resp_json['result']) == 0:
121            self._maybe_delete_trans(th)
122        return resp_json['result']
123
124    def get_schema(self, **kwargs):
125        payload = {'method': 'get_schema', 'params': kwargs}
126        resp, resp_json = self._maybe_write_call(payload)
127        return resp_json['result']
128
129    def get_module_prefix_map(self, path=None):
130        if path is None:
131            payload = {'method': 'get_module_prefix_map', 'params': {}}
132            resp, resp_json = self._call(payload)
133        else:
134            payload = {'method': 'get_module_prefix_map', 'params': {'path': path}}
135            resp, resp_json = self._maybe_write_call(payload)
136        return resp_json['result']
137
138    def get_value(self, path):
139        payload = {
140            'method': 'get_value',
141            'params': {'path': path}
142        }
143        resp, resp_json = self._read_call(payload)
144        return resp_json['result']
145
146    def exists(self, path):
147        payload = {'method': 'exists', 'params': {'path': path}}
148        try:
149            resp, resp_json = self._read_call(payload)
150            return resp_json['result']['exists']
151        except NsoException as ex:
152            # calling exists on a sub-list when the parent list does
153            # not exists will cause data.not_found errors on recent
154            # NSO
155            if 'type' in ex.error and ex.error['type'] == 'data.not_found':
156                return False
157            raise
158
159    def create(self, th, path):
160        payload = {'method': 'create', 'params': {'th': th, 'path': path}}
161        self._write_call(payload)
162
163    def delete(self, th, path):
164        payload = {'method': 'delete', 'params': {'th': th, 'path': path}}
165        self._write_call(payload)
166
167    def set_value(self, th, path, value):
168        payload = {
169            'method': 'set_value',
170            'params': {'th': th, 'path': path, 'value': value}
171        }
172        resp, resp_json = self._write_call(payload)
173        return resp_json['result']
174
175    def show_config(self, path, operational=False):
176        payload = {
177            'method': 'show_config',
178            'params': {
179                'path': path,
180                'result_as': 'json',
181                'with_oper': operational}
182        }
183        resp, resp_json = self._read_call(payload)
184        return resp_json['result']
185
186    def query(self, xpath, fields):
187        payload = {
188            'method': 'query',
189            'params': {
190                'xpath_expr': xpath,
191                'selection': fields
192            }
193        }
194        resp, resp_json = self._read_call(payload)
195        return resp_json['result']['results']
196
197    def run_action(self, th, path, params=None):
198        if params is None:
199            params = {}
200
201        if is_version(self, [(4, 5), (4, 4, 3)]):
202            result_format = 'json'
203        else:
204            result_format = 'normal'
205
206        payload = {
207            'method': 'run_action',
208            'params': {
209                'format': result_format,
210                'path': path,
211                'params': params
212            }
213        }
214        if th is None:
215            resp, resp_json = self._read_call(payload)
216        else:
217            payload['params']['th'] = th
218            resp, resp_json = self._call(payload)
219
220        if result_format == 'normal':
221            # this only works for one-level results, list entries,
222            # containers etc will have / in their name.
223            result = {}
224            for info in resp_json['result']:
225                result[info['name']] = info['value']
226        else:
227            result = resp_json['result']
228
229        return result
230
231    def _call(self, payload):
232        self._id += 1
233        if 'id' not in payload:
234            payload['id'] = self._id
235
236        if 'jsonrpc' not in payload:
237            payload['jsonrpc'] = '2.0'
238
239        data = json.dumps(payload)
240        try:
241            resp = open_url(
242                self._url, timeout=self._timeout,
243                method='POST', data=data, headers=self._headers,
244                validate_certs=self._validate_certs)
245            if resp.code != 200:
246                raise NsoException(
247                    'NSO returned HTTP code {0}, expected 200'.format(resp.status), {})
248        except socket.timeout:
249            raise NsoException('request timed out against NSO at {0}'.format(self._url), {})
250
251        resp_body = resp.read()
252        resp_json = json.loads(resp_body)
253
254        if 'error' in resp_json:
255            self._handle_call_error(payload, resp_json)
256        return resp, resp_json
257
258    def _handle_call_error(self, payload, resp_json):
259        method = payload['method']
260
261        error = resp_json['error']
262        error_type = error['type'][len('rpc.method.'):]
263        if error_type in ('unexpected_params',
264                          'unknown_params_value',
265                          'invalid_params',
266                          'invalid_params_type',
267                          'data_not_found'):
268            key = error['data']['param']
269            error_type_s = error_type.replace('_', ' ')
270            if key == 'path':
271                msg = 'NSO {0} {1}. path = {2}'.format(
272                    method, error_type_s, payload['params']['path'])
273            else:
274                path = payload['params'].get('path', 'unknown')
275                msg = 'NSO {0} {1}. path = {2}. {3} = {4}'.format(
276                    method, error_type_s, path, key, payload['params'][key])
277        else:
278            msg = 'NSO {0} returned JSON-RPC error: {1}'.format(method, error)
279
280        raise NsoException(msg, error)
281
282    def _read_call(self, payload):
283        if 'th' not in payload['params']:
284            payload['params']['th'] = self.get_trans(mode='read')
285        return self._call(payload)
286
287    def _write_call(self, payload):
288        if 'th' not in payload['params']:
289            payload['params']['th'] = self.get_trans(mode='read_write')
290        return self._call(payload)
291
292    def _maybe_write_call(self, payload):
293        if 'read_write' in self._trans:
294            return self._write_call(payload)
295        else:
296            return self._read_call(payload)
297
298    def _maybe_delete_trans(self, th):
299        for mode in ('read', 'read_write'):
300            if th == self._trans.get(mode, None):
301                del self._trans[mode]
302
303
304class ValueBuilder(object):
305    PATH_RE = re.compile('{[^}]*}')
306    PATH_RE_50 = re.compile('{[^}]*}$')
307
308    class Value(object):
309        __slots__ = ['path', 'tag_path', 'state', 'value', 'deps']
310
311        def __init__(self, path, state, value, deps):
312            self.path = path
313            self.tag_path = ValueBuilder.PATH_RE.sub('', path)
314            self.state = state
315            self.value = value
316            self.deps = deps
317
318            # nodes can depend on themselves
319            if self.tag_path in self.deps:
320                self.deps.remove(self.tag_path)
321
322        def __lt__(self, rhs):
323            l_len = len(self.path.split('/'))
324            r_len = len(rhs.path.split('/'))
325            if l_len == r_len:
326                return self.path.__lt__(rhs.path)
327            return l_len < r_len
328
329        def __str__(self):
330            return 'Value<path={0}, state={1}, value={2}>'.format(
331                self.path, self.state, self.value)
332
333    class ValueIterator(object):
334        def __init__(self, client, values, delayed_values):
335            self._client = client
336            self._values = values
337            self._delayed_values = delayed_values
338            self._pos = 0
339
340        def __iter__(self):
341            return self
342
343        def __next__(self):
344            return self.next()
345
346        def next(self):
347            if self._pos >= len(self._values):
348                if len(self._delayed_values) == 0:
349                    raise StopIteration()
350
351                builder = ValueBuilder(self._client, delay=False)
352                for (parent, maybe_qname, value) in self._delayed_values:
353                    builder.build(parent, maybe_qname, value)
354                del self._delayed_values[:]
355                self._values.extend(builder.values)
356
357                return self.next()
358
359            value = self._values[self._pos]
360            self._pos += 1
361            return value
362
363    def __init__(self, client, mode='config', delay=None):
364        self._client = client
365        self._mode = mode
366        self._schema_cache = {}
367        self._module_prefix_map_cache = {}
368        self._values = []
369        self._values_dirty = False
370        self._delay = delay is None and mode == 'config' and is_version(self._client, [(5, 0)])
371        self._delayed_values = []
372
373    def build(self, parent, maybe_qname, value, schema=None):
374        qname, name = self.get_prefix_name(parent, maybe_qname)
375        if name is None:
376            path = parent
377        else:
378            path = '{0}/{1}'.format(parent, qname)
379
380        if schema is None:
381            schema = self._get_schema(path)
382
383        if self._delay and schema.get('is_mount_point', False):
384            # delay conversion of mounted values, required to get
385            # shema information on 5.0 and later.
386            self._delayed_values.append((parent, maybe_qname, value))
387        elif self._is_leaf_list(schema) and is_version(self._client, [(4, 5)]):
388            self._build_leaf_list(path, schema, value)
389        elif self._is_leaf(schema):
390            deps = schema.get('deps', [])
391            if self._is_empty_leaf(schema):
392                exists = self._client.exists(path)
393                if exists and value != [None]:
394                    self._add_value(path, State.ABSENT, None, deps)
395                elif not exists and value == [None]:
396                    self._add_value(path, State.PRESENT, None, deps)
397            else:
398                if maybe_qname is None:
399                    value_type = self.get_type(path)
400                else:
401                    value_type = self._get_child_type(parent, qname)
402
403                if 'identityref' in value_type:
404                    if isinstance(value, list):
405                        value = [ll_v for ll_v, t_ll_v
406                                 in [self.get_prefix_name(parent, v) for v in value]]
407                    else:
408                        value, t_value = self.get_prefix_name(parent, value)
409                self._add_value(path, State.SET, value, deps)
410        elif isinstance(value, dict):
411            self._build_dict(path, schema, value)
412        elif isinstance(value, list):
413            self._build_list(path, schema, value)
414        else:
415            raise ModuleFailException(
416                'unsupported schema {0} at {1}'.format(
417                    schema['kind'], path))
418
419    @property
420    def values(self):
421        if self._values_dirty:
422            self._values = ValueBuilder.sort_values(self._values)
423            self._values_dirty = False
424
425        return ValueBuilder.ValueIterator(self._client, self._values, self._delayed_values)
426
427    @staticmethod
428    def sort_values(values):
429        class N(object):
430            def __init__(self, v):
431                self.tmp_mark = False
432                self.mark = False
433                self.v = v
434
435        sorted_values = []
436        nodes = [N(v) for v in sorted(values)]
437
438        def get_node(tag_path):
439            return next((m for m in nodes
440                         if m.v.tag_path == tag_path), None)
441
442        def is_cycle(n, dep, visited):
443            visited.add(n.v.tag_path)
444            if dep in visited:
445                return True
446
447            dep_n = get_node(dep)
448            if dep_n is not None:
449                for sub_dep in dep_n.v.deps:
450                    if is_cycle(dep_n, sub_dep, visited):
451                        return True
452
453            return False
454
455        # check for dependency cycles, remove if detected. sort will
456        # not be 100% but allows for a best-effort to work around
457        # issue in NSO.
458        for n in nodes:
459            for dep in n.v.deps:
460                if is_cycle(n, dep, set()):
461                    n.v.deps.remove(dep)
462
463        def visit(n):
464            if n.tmp_mark:
465                return False
466            if not n.mark:
467                n.tmp_mark = True
468                for m in nodes:
469                    if m.v.tag_path in n.v.deps:
470                        if not visit(m):
471                            return False
472
473                n.tmp_mark = False
474                n.mark = True
475
476                sorted_values.insert(0, n.v)
477
478            return True
479
480        n = next((n for n in nodes if not n.mark), None)
481        while n is not None:
482            visit(n)
483            n = next((n for n in nodes if not n.mark), None)
484
485        return sorted_values[::-1]
486
487    def _build_dict(self, path, schema, value):
488        keys = schema.get('key', [])
489        for dict_key, dict_value in value.items():
490            qname, name = self.get_prefix_name(path, dict_key)
491            if dict_key in ('__state', ) or name in keys:
492                continue
493
494            child_schema = self._find_child(path, schema, qname)
495            self.build(path, dict_key, dict_value, child_schema)
496
497    def _build_leaf_list(self, path, schema, value):
498        deps = schema.get('deps', [])
499        entry_type = self.get_type(path, schema)
500
501        if self._mode == 'verify':
502            for entry in value:
503                if 'identityref' in entry_type:
504                    entry, t_entry = self.get_prefix_name(path, entry)
505                entry_path = '{0}{{{1}}}'.format(path, entry)
506                if not self._client.exists(entry_path):
507                    self._add_value(entry_path, State.ABSENT, None, deps)
508        else:
509            # remove leaf list if treated as a list and then re-create the
510            # expected list entries.
511            self._add_value(path, State.ABSENT, None, deps)
512
513            for entry in value:
514                if 'identityref' in entry_type:
515                    entry, t_entry = self.get_prefix_name(path, entry)
516                entry_path = '{0}{{{1}}}'.format(path, entry)
517                self._add_value(entry_path, State.PRESENT, None, deps)
518
519    def _build_list(self, path, schema, value):
520        deps = schema.get('deps', [])
521        for entry in value:
522            entry_key = self._build_key(path, entry, schema['key'])
523            entry_path = '{0}{{{1}}}'.format(path, entry_key)
524            entry_state = entry.get('__state', 'present')
525            entry_exists = self._client.exists(entry_path)
526
527            if entry_state == 'absent':
528                if entry_exists:
529                    self._add_value(entry_path, State.ABSENT, None, deps)
530            else:
531                if not entry_exists:
532                    self._add_value(entry_path, State.PRESENT, None, deps)
533                if entry_state in State.SYNC_STATES:
534                    self._add_value(entry_path, entry_state, None, deps)
535
536            self.build(entry_path, None, entry)
537
538    def _build_key(self, path, entry, schema_keys):
539        key_parts = []
540        for key in schema_keys:
541            value = entry.get(key, None)
542            if value is None:
543                raise ModuleFailException(
544                    'required leaf {0} in {1} not set in data'.format(
545                        key, path))
546
547            value_type = self._get_child_type(path, key)
548            if 'identityref' in value_type:
549                value, t_value = self.get_prefix_name(path, value)
550            key_parts.append(self._quote_key(value))
551        return ' '.join(key_parts)
552
553    def _quote_key(self, key):
554        if isinstance(key, bool):
555            return key and 'true' or 'false'
556
557        q_key = []
558        for c in str(key):
559            if c in ('{', '}', "'", '\\'):
560                q_key.append('\\')
561            q_key.append(c)
562        q_key = ''.join(q_key)
563        if ' ' in q_key:
564            return '"{0}"'.format(q_key)
565        return q_key
566
567    def _find_child(self, path, schema, qname):
568        if 'children' not in schema:
569            schema = self._get_schema(path)
570
571        # look for the qualified name if : is in the name
572        child_schema = self._get_child(schema, qname)
573        if child_schema is not None:
574            return child_schema
575
576        # no child was found, look for a choice with a child matching
577        for child_schema in schema['children']:
578            if child_schema['kind'] != 'choice':
579                continue
580            choice_child_schema = self._get_choice_child(child_schema, qname)
581            if choice_child_schema is not None:
582                return choice_child_schema
583
584        raise ModuleFailException(
585            'no child in {0} with name {1}. children {2}'.format(
586                path, qname, ','.join((c.get('qname', c.get('name', None)) for c in schema['children']))))
587
588    def _add_value(self, path, state, value, deps):
589        self._values.append(ValueBuilder.Value(path, state, value, deps))
590        self._values_dirty = True
591
592    def get_prefix_name(self, path, qname):
593        if not isinstance(qname, (str, unicode)):
594            return qname, None
595        if ':' not in qname:
596            return qname, qname
597
598        module_prefix_map = self._get_module_prefix_map(path)
599        module, name = qname.split(':', 1)
600        if module not in module_prefix_map:
601            raise ModuleFailException(
602                'no module mapping for module {0}. loaded modules {1}'.format(
603                    module, ','.join(sorted(module_prefix_map.keys()))))
604
605        return '{0}:{1}'.format(module_prefix_map[module], name), name
606
607    def _get_schema(self, path):
608        return self._ensure_schema_cached(path)['data']
609
610    def _get_child_type(self, parent_path, key):
611        all_schema = self._ensure_schema_cached(parent_path)
612        parent_schema = all_schema['data']
613        meta = all_schema['meta']
614        schema = self._find_child(parent_path, parent_schema, key)
615        return self.get_type(parent_path, schema, meta)
616
617    def get_type(self, path, schema=None, meta=None):
618        if schema is None or meta is None:
619            all_schema = self._ensure_schema_cached(path)
620            schema = all_schema['data']
621            meta = all_schema['meta']
622
623        if self._is_leaf(schema):
624            def get_type(meta, curr_type):
625                if curr_type.get('primitive', False):
626                    return [curr_type['name']]
627                if 'namespace' in curr_type:
628                    curr_type_key = '{0}:{1}'.format(
629                        curr_type['namespace'], curr_type['name'])
630                    type_info = meta['types'][curr_type_key][-1]
631                    return get_type(meta, type_info)
632                if 'leaf_type' in curr_type:
633                    return get_type(meta, curr_type['leaf_type'][-1])
634                if 'union' in curr_type:
635                    union_types = []
636                    for union_type in curr_type['union']:
637                        union_types.extend(get_type(meta, union_type[-1]))
638                    return union_types
639                return [curr_type.get('name', 'unknown')]
640
641            return get_type(meta, schema['type'])
642        return None
643
644    def _ensure_schema_cached(self, path):
645        if not self._delay and is_version(self._client, [(5, 0)]):
646            # newer versions of NSO support multiple different schemas
647            # for different devices, thus the device is required to
648            # look up the schema. Remove the key entry to get schema
649            # logic working ok.
650            path = ValueBuilder.PATH_RE_50.sub('', path)
651        else:
652            path = ValueBuilder.PATH_RE.sub('', path)
653
654        if path not in self._schema_cache:
655            schema = self._client.get_schema(path=path, levels=1)
656            self._schema_cache[path] = schema
657        return self._schema_cache[path]
658
659    def _get_module_prefix_map(self, path):
660        # newer versions of NSO support multiple mappings from module
661        # to prefix depending on which device is used.
662        if path != '' and is_version(self._client, [(5, 0)]):
663            if path not in self._module_prefix_map_cache:
664                self._module_prefix_map_cache[path] = self._client.get_module_prefix_map(path)
665            return self._module_prefix_map_cache[path]
666
667        if '' not in self._module_prefix_map_cache:
668            self._module_prefix_map_cache[''] = self._client.get_module_prefix_map()
669        return self._module_prefix_map_cache['']
670
671    def _get_child(self, schema, qname):
672        # no child specified, return parent
673        if qname is None:
674            return schema
675
676        name_key = ':' in qname and 'qname' or 'name'
677        return next((c for c in schema['children']
678                     if c.get(name_key, None) == qname), None)
679
680    def _get_choice_child(self, schema, qname):
681        name_key = ':' in qname and 'qname' or 'name'
682        for child_case in schema['cases']:
683            # look for direct child
684            choice_child_schema = next(
685                (c for c in child_case['children']
686                 if c.get(name_key, None) == qname), None)
687            if choice_child_schema is not None:
688                return choice_child_schema
689
690            # look for nested choice
691            for child_schema in child_case['children']:
692                if child_schema['kind'] != 'choice':
693                    continue
694                choice_child_schema = self._get_choice_child(child_schema, qname)
695                if choice_child_schema is not None:
696                    return choice_child_schema
697        return None
698
699    def _is_leaf_list(self, schema):
700        return schema.get('kind', None) == 'leaf-list'
701
702    def _is_leaf(self, schema):
703        # still checking for leaf-list here to be compatible with pre
704        # 4.5 versions of NSO.
705        return schema.get('kind', None) in ('key', 'leaf', 'leaf-list')
706
707    def _is_empty_leaf(self, schema):
708        return (schema.get('kind', None) == 'leaf' and
709                schema['type'].get('primitive', False) and
710                schema['type'].get('name', '') == 'empty')
711
712
713def connect(params):
714    client = JsonRpc(params['url'],
715                     params['timeout'],
716                     params['validate_certs'])
717    client.login(params['username'], params['password'])
718    return client
719
720
721def verify_version(client, required_versions):
722    version_str = client.get_system_setting('version')
723    if not verify_version_str(version_str, required_versions):
724        supported_versions = ', '.join(
725            ['.'.join([str(p) for p in required_version])
726             for required_version in required_versions])
727        raise ModuleFailException(
728            'unsupported NSO version {0}. {1} or later supported'.format(
729                version_str, supported_versions))
730
731
732def is_version(client, required_versions):
733    version_str = client.get_system_setting('version')
734    return verify_version_str(version_str, required_versions)
735
736
737def verify_version_str(version_str, required_versions):
738    version_str = re.sub('_.*', '', version_str)
739
740    version = [int(p) for p in version_str.split('.')]
741    if len(version) < 2:
742        raise ModuleFailException(
743            'unsupported NSO version format {0}'.format(version_str))
744
745    def check_version(required_version, version):
746        for pos in range(len(required_version)):
747            if pos >= len(version):
748                return False
749            if version[pos] > required_version[pos]:
750                return True
751            if version[pos] < required_version[pos]:
752                return False
753        return True
754
755    for required_version in required_versions:
756        if check_version(required_version, version):
757            return True
758    return False
759
760
761def normalize_value(expected_value, value, key):
762    if value is None:
763        return None
764    if (isinstance(expected_value, bool) and
765            isinstance(value, (str, unicode))):
766        return value == 'true'
767    if isinstance(expected_value, int):
768        try:
769            return int(value)
770        except TypeError:
771            raise ModuleFailException(
772                'returned value {0} for {1} is not a valid integer'.format(
773                    key, value))
774    if isinstance(expected_value, float):
775        try:
776            return float(value)
777        except TypeError:
778            raise ModuleFailException(
779                'returned value {0} for {1} is not a valid float'.format(
780                    key, value))
781    if isinstance(expected_value, (list, tuple)):
782        if not isinstance(value, (list, tuple)):
783            raise ModuleFailException(
784                'returned value {0} for {1} is not a list'.format(value, key))
785        if len(expected_value) != len(value):
786            raise ModuleFailException(
787                'list length mismatch for {0}'.format(key))
788
789        normalized_value = []
790        for i in range(len(expected_value)):
791            normalized_value.append(
792                normalize_value(expected_value[i], value[i], '{0}[{1}]'.format(key, i)))
793        return normalized_value
794
795    if isinstance(expected_value, dict):
796        if not isinstance(value, dict):
797            raise ModuleFailException(
798                'returned value {0} for {1} is not a dict'.format(value, key))
799        if len(expected_value) != len(value):
800            raise ModuleFailException(
801                'dict length mismatch for {0}'.format(key))
802
803        normalized_value = {}
804        for k in expected_value.keys():
805            n_k = normalize_value(k, k, '{0}[{1}]'.format(key, k))
806            if n_k not in value:
807                raise ModuleFailException('missing {0} in value'.format(n_k))
808            normalized_value[n_k] = normalize_value(expected_value[k], value[k], '{0}[{1}]'.format(key, k))
809        return normalized_value
810
811    if HAVE_UNICODE:
812        if isinstance(expected_value, unicode) and isinstance(value, str):
813            return value.decode('utf-8')
814        if isinstance(expected_value, str) and isinstance(value, unicode):
815            return value.encode('utf-8')
816    else:
817        if hasattr(expected_value, 'encode') and hasattr(value, 'decode'):
818            return value.decode('utf-8')
819        if hasattr(expected_value, 'decode') and hasattr(value, 'encode'):
820            return value.encode('utf-8')
821
822    return value
823