github.com/SUSE/skuba@v1.4.17/skuba-update/test/unit/skuba_update_test.py (about) 1 #!/usr/bin/env python 2 # -*- encoding: utf-8 -*- 3 4 # Copyright (c) 2019 SUSE LLC. 5 # 6 # Licensed under the Apache License, Version 2.0 (the "License"); 7 # you may not use this file except in compliance with the License. 8 # You may obtain a copy of the License at 9 # 10 # http://www.apache.org/licenses/LICENSE-2.0 11 # 12 # Unless required by applicable law or agreed to in writing, software 13 # distributed under the License is distributed on an "AS IS" BASIS, 14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 # See the License for the specific language governing permissions and 16 # limitations under the License. 17 18 import json 19 from collections import namedtuple 20 21 from mock import patch, call, mock_open, Mock, ANY 22 from skuba_update.skuba_update import ( 23 main, 24 update, 25 run_command, 26 run_zypper_command, 27 node_name_from_machine_id, 28 annotate, 29 is_reboot_needed, 30 reboot_sentinel_file, 31 annotate_updates_available, 32 annotate_caasp_release_version, 33 get_update_list, 34 restart_services, 35 REBOOT_REQUIRED_PATH, 36 ZYPPER_EXIT_INF_UPDATE_NEEDED, 37 ZYPPER_EXIT_INF_RESTART_NEEDED, 38 ZYPPER_EXIT_INF_REBOOT_NEEDED, 39 KUBE_UPDATES_KEY, 40 KUBE_SECURITY_UPDATES_KEY, 41 KUBE_DISRUPTIVE_UPDATES_KEY, 42 KUBE_CAASP_RELEASE_VERSION_KEY 43 ) 44 45 46 @patch('subprocess.Popen') 47 def test_run_command(mock_subprocess): 48 mock_process = Mock() 49 mock_process.communicate.return_value = (b'stdout', b'stderr') 50 mock_process.returncode = 0 51 mock_subprocess.return_value = mock_process 52 result = run_command(['/bin/dummycmd', 'arg1']) 53 assert result.output == "stdout" 54 assert result.returncode == 0 55 assert result.error == 'stderr' 56 57 mock_process.returncode = 1 58 result = run_command(['/bin/dummycmd', 'arg1']) 59 assert result.output == "stdout" 60 assert result.returncode == 1 61 62 mock_process.communicate.return_value = (b'', b'stderr') 63 result = run_command(['/bin/dummycmd', 'arg1']) 64 assert result.output == "" 65 assert result.returncode == 1 66 67 68 @patch('argparse.ArgumentParser.parse_args') 69 @patch('subprocess.Popen') 70 def test_main_wrong_version(mock_subprocess, mock_args): 71 mock_process = Mock() 72 mock_process.communicate.return_value = (b'zypper 1.13.0', b'stderr') 73 mock_process.returncode = 0 74 mock_subprocess.return_value = mock_process 75 exception = False 76 try: 77 main() 78 except Exception as e: 79 exception = True 80 assert 'higher is required' in str(e) 81 assert exception 82 83 84 @patch('argparse.ArgumentParser.parse_args') 85 @patch('subprocess.Popen') 86 def test_main_bad_format_version(mock_subprocess, mock_args): 87 mock_process = Mock() 88 mock_process.communicate.return_value = (b'zypper', b'stderr') 89 mock_process.returncode = 0 90 mock_subprocess.return_value = mock_process 91 exception = False 92 try: 93 main() 94 except Exception as e: 95 exception = True 96 assert 'Could not parse' in str(e) 97 assert exception 98 99 100 @patch('argparse.ArgumentParser.parse_args') 101 @patch('subprocess.Popen') 102 def test_main_no_root(mock_subprocess, mock_args): 103 mock_process = Mock() 104 mock_process.communicate.return_value = (b'zypper 1.14.15', b'stderr') 105 mock_process.returncode = 0 106 mock_subprocess.return_value = mock_process 107 exception = False 108 try: 109 main() 110 except Exception as e: 111 exception = True 112 assert 'root privileges' in str(e) 113 assert exception 114 115 116 @patch('skuba_update.skuba_update.node_name_from_machine_id') 117 @patch('skuba_update.skuba_update.annotate_caasp_release_version') 118 @patch('skuba_update.skuba_update.annotate_updates_available') 119 @patch('argparse.ArgumentParser.parse_args') 120 @patch('os.environ.get', new={}.get, spec_set=True) 121 @patch('os.geteuid') 122 @patch('subprocess.Popen') 123 def test_main( 124 mock_subprocess, mock_geteuid, mock_args, 125 mock_annotate, mock_annotate_version, mock_name 126 ): 127 return_values = [ 128 (b'some_service1\nsome_service2', b''), 129 (b'zypper 1.14.15', b'') 130 ] 131 132 def mock_communicate(): 133 if len(return_values) > 1: 134 return return_values.pop() 135 else: 136 return return_values[0] 137 138 args = Mock() 139 args.annotate_only = False 140 mock_args.return_value = args 141 mock_geteuid.return_value = 0 142 mock_process = Mock() 143 mock_process.communicate.side_effect = mock_communicate 144 mock_process.returncode = 0 145 mock_subprocess.return_value = mock_process 146 main() 147 assert mock_subprocess.call_args_list == [ 148 call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY), 149 call( 150 ['zypper', '--userdata', 'skuba-update', 'ref', '-s'], 151 stdout=None, stderr=None, env=ANY 152 ), 153 call([ 154 'zypper', '--userdata', 'skuba-update', '--non-interactive', 155 '--non-interactive-include-reboot-patches', 'patch' 156 ], stdout=None, stderr=None, env=ANY), 157 call( 158 ['zypper', '--userdata', 'skuba-update', 'ps', '-sss'], 159 stdout=-1, stderr=-1, env=ANY 160 ), 161 call( 162 ['systemctl', 'restart', 'some_service1'], 163 stdout=None, stderr=None, env=ANY 164 ), 165 call( 166 ['systemctl', 'restart', 'some_service2'], 167 stdout=None, stderr=None, env=ANY 168 ), 169 call( 170 ['zypper', '--userdata', 'skuba-update', 'needs-rebooting'], 171 stdout=None, stderr=None, env=ANY 172 ), 173 ] 174 175 176 @patch('subprocess.Popen') 177 @patch('skuba_update.skuba_update.run_zypper_command') 178 def test_restart_services_error(mock_zypp_cmd, mock_subprocess, capsys): 179 command_type = namedtuple( 180 'command', ['output', 'error', 'returncode'] 181 ) 182 183 mock_process = Mock() 184 mock_process.communicate.return_value = (b'', b'restart error msg') 185 mock_process.returncode = 1 186 mock_subprocess.return_value = mock_process 187 188 mock_zypp_cmd.return_value = command_type( 189 output="service1\nservice2", 190 error='', 191 returncode=0 192 ) 193 194 restart_services() 195 out, err = capsys.readouterr() 196 assert 'returned non zero exit code' in out 197 198 199 @patch('skuba_update.skuba_update.node_name_from_machine_id') 200 @patch('skuba_update.skuba_update.annotate_updates_available') 201 @patch('argparse.ArgumentParser.parse_args') 202 @patch('os.environ.get', new={}.get, spec_set=True) 203 @patch('os.geteuid') 204 @patch('subprocess.Popen') 205 def test_main_annotate_only( 206 mock_subprocess, mock_geteuid, mock_args, mock_annotate, mock_name 207 ): 208 args = Mock() 209 args.annotate_only = True 210 mock_args.return_value = args 211 mock_geteuid.return_value = 0 212 mock_process = Mock() 213 mock_process.communicate.return_value = (b'zypper 1.14.15', b'stderr') 214 mock_process.returncode = ZYPPER_EXIT_INF_UPDATE_NEEDED 215 mock_subprocess.return_value = mock_process 216 main() 217 assert mock_subprocess.call_args_list == [ 218 call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY), 219 call( 220 ['zypper', '--userdata', 'skuba-update', 'ref', '-s'], 221 stdout=None, stderr=None, env=ANY 222 ), 223 call([ 224 'rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}' 225 ], stdout=-1, stderr=-1, env=ANY), 226 ] 227 228 229 @patch('skuba_update.skuba_update.node_name_from_machine_id') 230 @patch('skuba_update.skuba_update.annotate_updates_available') 231 @patch('argparse.ArgumentParser.parse_args') 232 @patch('os.environ.get', new={}.get, spec_set=True) 233 @patch('os.geteuid') 234 @patch('subprocess.Popen') 235 def test_main_zypper_returns_100( 236 mock_subprocess, mock_geteuid, mock_args, mock_annotate, mock_name 237 ): 238 return_values = [(b'', b''), (b'zypper 1.14.15', b'')] 239 240 def mock_communicate(): 241 if len(return_values) > 1: 242 return return_values.pop() 243 else: 244 return return_values[0] 245 246 args = Mock() 247 args.annotate_only = False 248 mock_args.return_value = args 249 mock_geteuid.return_value = 0 250 mock_process = Mock() 251 mock_process.communicate.side_effect = mock_communicate 252 mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED 253 mock_subprocess.return_value = mock_process 254 main() 255 assert mock_subprocess.call_args_list == [ 256 call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY), 257 call([ 258 'zypper', '--userdata', 'skuba-update', 'ref', '-s' 259 ], stdout=None, stderr=None, env=ANY), 260 call([ 261 'zypper', '--userdata', 'skuba-update', '--non-interactive', 262 '--non-interactive-include-reboot-patches', 'patch' 263 ], stdout=None, stderr=None, env=ANY), 264 call([ 265 'zypper', '--userdata', 'skuba-update', '--non-interactive', 266 '--non-interactive-include-reboot-patches', 'patch' 267 ], stdout=None, stderr=None, env=ANY), 268 call( 269 ['zypper', '--userdata', 'skuba-update', 'ps', '-sss'], 270 stdout=-1, stderr=-1, env=ANY 271 ), 272 call([ 273 'rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}' 274 ], stdout=-1, stderr=-1, env=ANY), 275 call([ 276 'zypper', '--userdata', 'skuba-update', 'needs-rebooting' 277 ], stdout=None, stderr=None, env=ANY), 278 ] 279 280 281 @patch('pathlib.Path.is_file') 282 @patch('subprocess.Popen') 283 def test_update_zypper_is_fine_but_created_reboot_required( 284 mock_subprocess, mock_is_file 285 ): 286 mock_process = Mock() 287 mock_process.communicate.return_value = (b'stdout', b'stderr') 288 289 mock_process.returncode = ZYPPER_EXIT_INF_REBOOT_NEEDED 290 mock_subprocess.return_value = mock_process 291 mock_is_file.return_value = True 292 293 exception = False 294 try: 295 reboot_sentinel_file(update()) 296 except PermissionError as e: 297 exception = True 298 msg = 'Permission denied: \'{0}\''.format(REBOOT_REQUIRED_PATH) 299 assert msg in str(e) 300 assert exception 301 302 303 @patch('subprocess.Popen') 304 def test_run_zypper_command(mock_subprocess): 305 mock_process = Mock() 306 mock_process.communicate.return_value = (b'stdout', b'stderr') 307 mock_process.returncode = 0 308 mock_subprocess.return_value = mock_process 309 assert run_zypper_command(['patch']) == 0 310 mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED 311 mock_subprocess.return_value = mock_process 312 assert run_zypper_command( 313 ['patch']) == ZYPPER_EXIT_INF_RESTART_NEEDED 314 315 316 @patch('subprocess.Popen') 317 def test_run_zypper_command_failure(mock_subprocess): 318 mock_process = Mock() 319 mock_process.communicate.return_value = (b'', b'') 320 mock_process.returncode = 1 321 mock_subprocess.return_value = mock_process 322 exception = False 323 try: 324 run_zypper_command(['patch']) == 'stdout' 325 except Exception as e: 326 exception = True 327 assert '"zypper --userdata skuba-update patch" failed' in str(e) 328 assert exception 329 330 331 @patch('builtins.open', 332 mock_open(read_data='9ea12911449eb7b5f8f228294bf9209a')) 333 @patch('subprocess.Popen') 334 @patch('json.loads') 335 def test_node_name_from_machine_id(mock_loads, mock_subprocess): 336 json_node_object = { 337 'items': [ 338 { 339 'metadata': { 340 'name': 'my-node-1' 341 }, 342 'status': { 343 'nodeInfo': { 344 'machineID': '49f8e2911a1449b7b5ef2bf92282909a' 345 } 346 } 347 }, 348 { 349 'metadata': { 350 'name': 'my-node-2' 351 }, 352 'status': { 353 'nodeInfo': { 354 'machineID': '9ea12911449eb7b5f8f228294bf9209a' 355 } 356 } 357 } 358 ] 359 } 360 breaking_json_node_object = {'Items': []} 361 362 mock_process = Mock() 363 mock_process.communicate.return_value = (json.dumps(json_node_object) 364 .encode(), b'') 365 mock_process.returncode = 0 366 mock_subprocess.return_value = mock_process 367 mock_loads.return_value = json_node_object 368 assert node_name_from_machine_id() == 'my-node-2' 369 370 json_node_object2 = json_node_object 371 json_node_object2['items'][1]['status']['nodeInfo']['machineID'] = \ 372 'another-id-that-doesnt-reflect-a-node' 373 mock_loads.return_value = json_node_object2 374 exception = False 375 try: 376 node_name_from_machine_id() == 'my-node-2' 377 except Exception as e: 378 exception = True 379 assert 'Node name could not be determined' in str(e) 380 assert exception 381 382 mock_loads.return_value = breaking_json_node_object 383 exception = False 384 try: 385 node_name_from_machine_id() == 'my-node-2' 386 except Exception as e: 387 exception = True 388 assert 'Unexpected format' in str(e) 389 assert exception 390 exception = False 391 mock_process.returncode = 1 392 try: 393 node_name_from_machine_id() == 'my-node' 394 except Exception as e: 395 exception = True 396 assert 'Kubectl failed getting nodes list' in str(e) 397 assert exception 398 399 400 @patch('subprocess.Popen') 401 def test_annotate(mock_subprocess, capsys): 402 mock_process = Mock() 403 mock_process.communicate.return_value = (b'node/my-node-1 annotated', 404 b'stderr') 405 mock_process.returncode = 0 406 mock_subprocess.return_value = mock_process 407 assert annotate( 408 'node', 'my-node-1', 409 KUBE_DISRUPTIVE_UPDATES_KEY, 'yes' 410 ) == 'node/my-node-1 annotated' 411 mock_process.returncode = 1 412 annotate( 413 'node', 'my-node-1', 414 KUBE_DISRUPTIVE_UPDATES_KEY, 'yes' 415 ) 416 out, err = capsys.readouterr() 417 assert 'Warning! kubectl returned non zero exit code' in out 418 419 420 @patch('skuba_update.skuba_update.node_name_from_machine_id') 421 @patch('skuba_update.skuba_update.annotate') 422 @patch('subprocess.Popen') 423 def test_annotate_updates_empty(mock_subprocess, mock_annotate, mock_name): 424 mock_name.return_value = 'mynode' 425 mock_process = Mock() 426 mock_process.communicate.return_value = ( 427 b'<stream><update-status><update-list>' 428 b'</update-list></update-status></stream>', b'' 429 ) 430 mock_process.returncode = 0 431 mock_subprocess.return_value = mock_process 432 annotate_updates_available(mock_name.return_value) 433 assert mock_subprocess.call_args_list == [ 434 call( 435 ['zypper', '--userdata', 'skuba-update', 436 '--non-interactive', '--xmlout', 'list-patches'], 437 stdout=-1, stderr=-1, env=ANY 438 ) 439 ] 440 assert mock_annotate.call_args_list == [ 441 call('node', 'mynode', KUBE_UPDATES_KEY, 'no'), 442 call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'), 443 call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no') 444 ] 445 446 447 @patch('skuba_update.skuba_update.node_name_from_machine_id') 448 @patch('skuba_update.skuba_update.annotate') 449 @patch('subprocess.Popen') 450 def test_annotate_updates(mock_subprocess, mock_annotate, mock_name): 451 mock_name.return_value = 'mynode' 452 mock_process = Mock() 453 mock_process.communicate.return_value = ( 454 b'<stream><update-status><update-list><update interactive="message">' 455 b'</update></update-list></update-status></stream>', b'' 456 ) 457 mock_process.returncode = 0 458 mock_subprocess.return_value = mock_process 459 annotate_updates_available(mock_name.return_value) 460 assert mock_subprocess.call_args_list == [ 461 call( 462 ['zypper', '--userdata', 'skuba-update', 463 '--non-interactive', '--xmlout', 'list-patches'], 464 stdout=-1, stderr=-1, env=ANY 465 ) 466 ] 467 assert mock_annotate.call_args_list == [ 468 call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'), 469 call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'), 470 call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'yes') 471 ] 472 473 474 @patch("skuba_update.skuba_update.node_name_from_machine_id") 475 @patch("builtins.open", read_data="aa59dc0c5fe84247a77c26780dd0b3fd") 476 @patch('subprocess.Popen') 477 def test_annotate_updates_available(mock_subprocess, mock_open, mock_name): 478 mock_name.return_value = 'mynode' 479 480 mock_process = Mock() 481 mock_process.communicate.return_value = ( 482 b'<stream><update-status><update-list><update interactive="message">' 483 b'</update></update-list></update-status></stream>', b'' 484 ) 485 mock_process.returncode = 0 486 mock_subprocess.return_value = mock_process 487 488 annotate_updates_available(mock_name.return_value) 489 490 assert mock_subprocess.call_args_list == [ 491 call( 492 ['zypper', '--userdata', 'skuba-update', 493 '--non-interactive', '--xmlout', 'list-patches'], 494 stdout=-1, stderr=-1, env=ANY 495 ), 496 call( 497 ["kubectl", "annotate", "--overwrite", "node", 498 "mynode", "caasp.suse.com/has-updates=yes"], 499 stdout=-1, stderr=-1, env=ANY 500 ), 501 call( 502 ["kubectl", "annotate", "--overwrite", "node", 503 "mynode", "caasp.suse.com/has-security-updates=no"], 504 stdout=-1, stderr=-1, env=ANY 505 ), 506 call( 507 ["kubectl", "annotate", "--overwrite", "node", 508 "mynode", "caasp.suse.com/has-disruptive-updates=yes"], 509 stdout=-1, stderr=-1, env=ANY 510 ) 511 ] 512 513 514 @patch('skuba_update.skuba_update.node_name_from_machine_id') 515 @patch('skuba_update.skuba_update.annotate') 516 @patch('subprocess.Popen') 517 def test_annotate_updates_bad_xml(mock_subprocess, mock_annotate, mock_name): 518 mock_name.return_value = 'mynode' 519 mock_process = Mock() 520 mock_process.communicate.return_value = ( 521 b'<update-status><update-list><update interactive="message">' 522 b'</update></update-list></update-status>', b'' 523 ) 524 mock_process.returncode = 0 525 mock_subprocess.return_value = mock_process 526 527 annotate_updates_available(mock_name.return_value) 528 assert mock_subprocess.call_args_list == [ 529 call( 530 ['zypper', '--userdata', 'skuba-update', 531 '--non-interactive', '--xmlout', 'list-patches'], 532 stdout=-1, stderr=-1, env=ANY 533 ) 534 ] 535 assert mock_annotate.call_args_list == [ 536 call('node', 'mynode', KUBE_UPDATES_KEY, 'no'), 537 call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'), 538 call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no') 539 ] 540 541 542 @patch('skuba_update.skuba_update.node_name_from_machine_id') 543 @patch('skuba_update.skuba_update.annotate') 544 @patch('subprocess.Popen') 545 def test_annotate_updates_security( 546 mock_subprocess, mock_annotate, mock_name 547 ): 548 mock_name.return_value = 'mynode' 549 mock_process = Mock() 550 mock_process.communicate.return_value = ( 551 b'<stream><update-status><update-list>' 552 b'<update interactive="false" category="security">' 553 b'</update></update-list></update-status></stream>', b'' 554 ) 555 mock_process.returncode = 0 556 mock_subprocess.return_value = mock_process 557 558 annotate_updates_available(mock_name.return_value) 559 assert mock_subprocess.call_args_list == [ 560 call( 561 ['zypper', '--userdata', 'skuba-update', 562 '--non-interactive', '--xmlout', 'list-patches'], 563 stdout=-1, stderr=-1, env=ANY 564 ) 565 ] 566 assert mock_annotate.call_args_list == [ 567 call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'), 568 call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'yes'), 569 call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no') 570 ] 571 572 573 @patch('skuba_update.skuba_update.node_name_from_machine_id') 574 @patch('skuba_update.skuba_update.annotate') 575 @patch('subprocess.Popen') 576 def test_annotate_updates_available_is_reboot( 577 mock_subprocess, mock_annotate, mock_name 578 ): 579 mock_name.return_value = 'mynode' 580 581 mock_process = Mock() 582 mock_process.communicate.return_value = ( 583 b'<stream><update-status><update-list><update interactive="reboot">' 584 b'</update></update-list></update-status></stream>', b'' 585 ) 586 mock_process.returncode = 0 587 mock_subprocess.return_value = mock_process 588 589 annotate_updates_available(mock_name.return_value) 590 assert mock_subprocess.call_args_list == [ 591 call( 592 ['zypper', '--userdata', 'skuba-update', 593 '--non-interactive', '--xmlout', 'list-patches'], 594 stdout=-1, stderr=-1, env=ANY 595 ) 596 ] 597 assert mock_annotate.call_args_list == [ 598 call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'), 599 call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'), 600 call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'yes') 601 ] 602 603 604 @patch('skuba_update.skuba_update.node_name_from_machine_id') 605 @patch('skuba_update.skuba_update.annotate') 606 @patch('subprocess.Popen') 607 def test_annotate_caasp_release_version( 608 mock_subprocess, mock_annotate, mock_name 609 ): 610 mock_name.return_value = 'mynode' 611 612 mock_process = Mock() 613 mock_process.communicate.return_value = ( 614 b'1.2.3', b'' 615 ) 616 mock_process.returncode = 0 617 mock_subprocess.return_value = mock_process 618 619 annotate_caasp_release_version(mock_name.return_value) 620 assert mock_subprocess.call_args_list == [ 621 call( 622 ['rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}'], 623 stdout=-1, stderr=-1, env=ANY 624 ) 625 ] 626 assert mock_annotate.call_args_list == [ 627 call('node', 'mynode', KUBE_CAASP_RELEASE_VERSION_KEY, '1.2.3'), 628 ] 629 630 631 @patch('subprocess.Popen') 632 def test_is_reboot_needed_truthy(mock_subprocess): 633 mock_process = Mock() 634 mock_process.communicate.return_value = (b'', b'') 635 mock_process.returncode = ZYPPER_EXIT_INF_REBOOT_NEEDED 636 mock_subprocess.return_value = mock_process 637 638 assert is_reboot_needed() 639 640 641 @patch('subprocess.Popen') 642 def test_is_reboot_needed_falsey(mock_subprocess): 643 mock_process = Mock() 644 mock_process.communicate.return_value = (b'', b'') 645 mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED 646 mock_subprocess.return_value = mock_process 647 648 assert not is_reboot_needed() 649 650 651 def test_get_update_list_bad_xml(): 652 assert get_update_list('<xml') is None