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