github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/acceptancetests/tests/test_deploy_stack.py (about) 1 from argparse import ( 2 Namespace, 3 ) 4 from contextlib import contextmanager 5 from datetime import ( 6 datetime, 7 timedelta, 8 ) 9 import json 10 import logging 11 import os 12 import subprocess 13 from tempfile import NamedTemporaryFile 14 from unittest import ( 15 TestCase, 16 ) 17 try: 18 from mock import ( 19 call, 20 MagicMock, 21 patch, 22 Mock, 23 ) 24 except ImportError: 25 from unittest.mock import ( 26 call, 27 MagicMock, 28 patch, 29 Mock, 30 ) 31 import yaml 32 33 from deploy_stack import ( 34 archive_logs, 35 assess_juju_relations, 36 assess_juju_exec, 37 assess_upgrade, 38 boot_context, 39 BootstrapManager, 40 check_token, 41 copy_remote_logs, 42 CreateController, 43 ExistingController, 44 deploy_dummy_stack, 45 deploy_job, 46 _deploy_job, 47 deploy_job_parse_args, 48 dump_env_logs, 49 dump_juju_timings, 50 _get_clients_to_upgrade, 51 iter_remote_machines, 52 get_remote_machines, 53 make_controller_strategy, 54 PublicController, 55 safe_print_status, 56 update_env, 57 wait_for_state_server_to_shutdown, 58 error_if_unclean, 59 ) 60 from jujupy import ( 61 fake_juju_client, 62 get_cache_path, 63 get_timeout_prefix, 64 JujuData, 65 ModelClient, 66 Machine, 67 ) 68 69 from jujupy.exceptions import ( 70 SoftDeadlineExceeded, 71 ) 72 from jujupy.status import ( 73 Status, 74 ) 75 from jujupy.wait_condition import ( 76 CommandTime, 77 ) 78 from remote import ( 79 _Remote, 80 remote_from_address, 81 SSHRemote, 82 winrm, 83 ) 84 from tests import ( 85 assert_juju_call, 86 FakeHomeTestCase, 87 FakePopen, 88 make_fake_juju_return, 89 observable_temp_file, 90 temp_os_env, 91 use_context, 92 ) 93 from utility import ( 94 LoggedException, 95 temp_dir, 96 ) 97 98 99 class DeployStackTestCase(FakeHomeTestCase): 100 101 log_level = logging.DEBUG 102 103 def test_assess_juju_exec(self): 104 env = JujuData('foo', {'type': 'nonlocal'}) 105 client = ModelClient(env, None, None) 106 response_ok = json.dumps( 107 [{"ubuntu/0":{"id":"9","results":{"return-code":0,"stdout":"Linux\n"},"status":"completed","timing":{"completed":"2021-01-04 20:12:11 +0000 UTC","enqueued":"2021-01-04 20:12:07 +0000 UTC","started":"2021-01-04 20:12:10 +0000 UTC"},"unit":"ubuntu/0"}, 108 "wordpress/0":{"id":"10","results":{"return-code":0,"stdout":"Linux\n"},"status":"completed","timing":{"completed":"2021-01-04 20:12:10 +0000 UTC","enqueued":"2021-01-04 20:12:07 +0000 UTC","started":"2021-01-04 20:12:10 +0000 UTC"},"unit":"wordpress/0"}}]) 109 response_err = json.dumps([ 110 {"ubuntu/0":{"id":"9","results":{"return-code":0,"stdout":"Linux\n"},"status":"completed","timing":{"completed":"2021-01-04 20:12:11 +0000 UTC","enqueued":"2021-01-04 20:12:07 +0000 UTC","started":"2021-01-04 20:12:10 +0000 UTC"},"unit":"ubuntu/0"}, 111 "wordpress/0": { "id": "16", "results": { "return-code": 127, "stderr": "/tmp/juju-exec117440111/script.sh: line 1: fail-test: command not found\n" }, "status": "completed", "timing": { "completed": "2021-01-04 20:31:42 +0000 UTC", "enqueued": "2021-01-04 20:31:40 +0000 UTC", "started": "2021-01-04 20:31:42 +0000 UTC" }, "unit": "wordpress/0" 112 }}]) 113 with patch.object(client, 'get_juju_output', autospec=True, 114 return_value=response_ok) as gjo_mock: 115 responses = assess_juju_exec(client) 116 for response in responses: 117 118 self.assertFalse(response.get('results').get('return-code', False)) 119 self.assertIn(response.get('unit'), ["ubuntu/0", "wordpress/0"]) 120 self.assertEqual(len(responses), 2) 121 gjo_mock.assert_called_once_with( 122 'exec', '--format', 'json', '--application', 123 'dummy-source,dummy-sink', 'uname') 124 with patch.object(client, 'get_juju_output', autospec=True, 125 return_value=response_err) as gjo_mock: 126 with self.assertRaises(ValueError): 127 responses = assess_juju_exec(client) 128 gjo_mock.assert_called_once_with( 129 'exec', '--format', 'json', '--application', 130 'dummy-source,dummy-sink', 'uname') 131 132 def test_safe_print_status(self): 133 env = JujuData('foo', {'type': 'nonlocal'}) 134 client = ModelClient(env, None, None) 135 error = subprocess.CalledProcessError(1, 'status', 'status error') 136 with patch.object(client, 'juju', autospec=True, 137 side_effect=[error]) as mock: 138 with patch.object(client, 'iter_model_clients', 139 return_value=[client]) as imc_mock: 140 safe_print_status(client) 141 mock.assert_called_once_with('status', ('--format', 'yaml')) 142 imc_mock.assert_called_once_with() 143 144 def test_safe_print_status_ignores_soft_deadline(self): 145 client = fake_juju_client() 146 client._backend._past_deadline = True 147 client.bootstrap() 148 149 def raise_exception(e): 150 raise e 151 152 try: 153 with patch('logging.exception', side_effect=raise_exception): 154 safe_print_status(client) 155 except SoftDeadlineExceeded: 156 self.fail('Raised SoftDeadlineExceeded.') 157 158 def test_update_env(self): 159 env = JujuData('foo', {'type': 'paas'}) 160 update_env( 161 env, 'bar', series='wacky', bootstrap_host='baz', 162 agent_url='url', agent_stream='devel') 163 self.assertEqual('bar', env.environment) 164 self.assertEqual('bar', env.get_option('name')) 165 self.assertEqual('wacky', env.get_option('default-series')) 166 self.assertEqual('baz', env.get_option('bootstrap-host')) 167 self.assertEqual('url', env.get_option('agent-metadata-url')) 168 self.assertEqual('devel', env.get_option('agent-stream')) 169 self.assertNotIn('region', env._config) 170 171 def test_update_env_region(self): 172 env = JujuData('foo', {'type': 'paas'}) 173 update_env(env, 'bar', region='region-foo') 174 self.assertEqual('region-foo', env.get_region()) 175 176 def test_update_env_region_none(self): 177 env = JujuData( 178 'foo', 179 {'type': 'paas', 'region': 'region-foo'}) 180 update_env(env, 'bar', region=None) 181 self.assertEqual('region-foo', env.get_region()) 182 183 def test_dump_juju_timings(self): 184 first_start = datetime(2017, 3, 22, 23, 36, 52, 0) 185 first_end = first_start + timedelta(seconds=2) 186 second_start = datetime(2017, 3, 22, 23, 40, 51, 0) 187 env = JujuData('foo', {'type': 'bar'}) 188 client = ModelClient(env, None, None) 189 client._backend.juju_timings.extend([ 190 CommandTime('command1', ['command1', 'arg1'], start=first_start), 191 CommandTime( 192 'command2', ['command2', 'arg1', 'arg2'], start=second_start) 193 ]) 194 client._backend.juju_timings[0].actual_completion(end=first_end) 195 expected = [ 196 { 197 'command': 'command1', 198 'full_args': ['command1', 'arg1'], 199 'start': first_start, 200 'end': first_end, 201 'total_seconds': 2, 202 }, 203 { 204 'command': 'command2', 205 'full_args': ['command2', 'arg1', 'arg2'], 206 'start': second_start, 207 'end': None, 208 'total_seconds': None, 209 } 210 ] 211 with temp_dir() as fake_dir: 212 dump_juju_timings(client, fake_dir) 213 with open(os.path.join(fake_dir, 214 'juju_command_times.yaml')) as out_file: 215 file_data = yaml.safe_load(out_file) 216 self.assertEqual(file_data, expected) 217 218 def test_check_token(self): 219 env = JujuData('foo', {'type': 'lxd'}) 220 client = ModelClient(env, None, None) 221 status = Status.from_text("""\ 222 applications: 223 dummy-sink: 224 units: 225 dummy-sink/0: 226 workload-status: 227 current: active 228 message: Token is token 229 230 """) 231 remote = SSHRemote(client, 'unit', None, series='bionic') 232 with patch('deploy_stack.remote_from_unit', autospec=True, 233 return_value=remote): 234 with patch.object(remote, 'cat', autospec=True, 235 return_value='token') as rc_mock: 236 with patch.object(client, 'get_status', autospec=True, 237 return_value=status): 238 check_token(client, 'token', timeout=0) 239 rc_mock.assert_called_once_with('/var/run/dummy-sink/token') 240 self.assertTrue(remote.use_juju_ssh) 241 self.assertEqual( 242 ['INFO Waiting for applications to reach ready.', 243 'INFO Retrieving token.', 244 "INFO Token matches expected 'token'"], 245 self.log_stream.getvalue().splitlines()) 246 247 def test_check_token_not_found(self): 248 env = JujuData('foo', {'type': 'lxd'}) 249 client = ModelClient(env, None, None) 250 status = Status.from_text("""\ 251 applications: 252 dummy-sink: 253 units: 254 dummy-sink/0: 255 workload-status: 256 current: active 257 message: Waiting for token 258 259 """) 260 remote = SSHRemote(client, 'unit', None, series='bionic') 261 error = subprocess.CalledProcessError(1, 'ssh', '') 262 with patch('deploy_stack.remote_from_unit', autospec=True, 263 return_value=remote): 264 with patch.object(remote, 'cat', autospec=True, 265 side_effect=error) as rc_mock: 266 with patch.object(remote, 'get_address', 267 autospec=True) as ga_mock: 268 with patch.object(client, 'get_status', autospec=True, 269 return_value=status): 270 with self.assertRaisesRegexp(ValueError, 271 "Token is ''"): 272 check_token(client, 'token', timeout=0) 273 self.assertEqual(2, rc_mock.call_count) 274 ga_mock.assert_called_once_with() 275 self.assertFalse(remote.use_juju_ssh) 276 self.assertEqual( 277 ['INFO Waiting for applications to reach ready.', 278 'INFO Retrieving token.'], 279 self.log_stream.getvalue().splitlines()) 280 281 def test_check_token_not_found_juju_ssh_broken(self): 282 env = JujuData('foo', {'type': 'lxd'}) 283 client = ModelClient(env, None, None) 284 status = Status.from_text("""\ 285 applications: 286 dummy-sink: 287 units: 288 dummy-sink/0: 289 workload-status: 290 current: active 291 message: Token is token 292 293 """) 294 remote = SSHRemote(client, 'unit', None, series='bionic') 295 error = subprocess.CalledProcessError(1, 'ssh', '') 296 with patch('deploy_stack.remote_from_unit', autospec=True, 297 return_value=remote): 298 with patch.object(remote, 'cat', autospec=True, 299 side_effect=[error, 'token']) as rc_mock: 300 with patch.object(remote, 'get_address', 301 autospec=True) as ga_mock: 302 with patch.object(client, 'get_status', autospec=True, 303 return_value=status): 304 with self.assertRaisesRegexp(ValueError, 305 "Token is 'token'"): 306 check_token(client, 'token', timeout=0) 307 self.assertEqual(2, rc_mock.call_count) 308 rc_mock.assert_called_with('/var/run/dummy-sink/token') 309 ga_mock.assert_called_once_with() 310 self.assertFalse(remote.use_juju_ssh) 311 self.assertEqual( 312 ['INFO Waiting for applications to reach ready.', 313 'INFO Retrieving token.', 314 "INFO Token matches expected 'token'", 315 'ERROR juju ssh to unit is broken.'], 316 self.log_stream.getvalue().splitlines()) 317 318 def test_check_token_win_status(self): 319 env = JujuData('foo', {'type': 'azure'}) 320 client = ModelClient(env, None, None) 321 remote = MagicMock(spec=['cat', 'is_windows']) 322 remote.is_windows.return_value = True 323 status = Status.from_text("""\ 324 applications: 325 dummy-sink: 326 units: 327 dummy-sink/0: 328 workload-status: 329 current: active 330 message: Token is token 331 332 """) 333 with patch('deploy_stack.remote_from_unit', autospec=True, 334 return_value=remote): 335 with patch.object(client, 'get_status', autospec=True, 336 return_value=status): 337 check_token(client, 'token', timeout=0) 338 # application-status had the token. 339 self.assertEqual(0, remote.cat.call_count) 340 self.assertEqual( 341 ['INFO Waiting for applications to reach ready.', 342 'INFO Retrieving token.', 343 "INFO Token matches expected 'token'"], 344 self.log_stream.getvalue().splitlines()) 345 346 def test_check_token_win_client_status(self): 347 env = JujuData('foo', {'type': 'ec2'}) 348 client = ModelClient(env, None, None) 349 remote = MagicMock(spec=['cat', 'is_windows']) 350 remote.is_windows.return_value = False 351 status = Status.from_text("""\ 352 applications: 353 dummy-sink: 354 units: 355 dummy-sink/0: 356 workload-status: 357 current: active 358 message: Token is token 359 360 """) 361 with patch('deploy_stack.remote_from_unit', autospec=True, 362 return_value=remote): 363 with patch.object(client, 'get_status', autospec=True, 364 return_value=status): 365 with patch('sys.platform', 'win32'): 366 check_token(client, 'token', timeout=0) 367 # application-status had the token. 368 self.assertEqual(0, remote.cat.call_count) 369 self.assertEqual( 370 ['INFO Waiting for applications to reach ready.', 371 'INFO Retrieving token.', 372 "INFO Token matches expected 'token'"], 373 self.log_stream.getvalue().splitlines()) 374 375 def test_check_token_win_remote(self): 376 env = JujuData('foo', {'type': 'azure'}) 377 client = ModelClient(env, None, None) 378 remote = MagicMock(spec=['cat', 'is_windows']) 379 remote.is_windows.return_value = True 380 remote.cat.return_value = 'token' 381 status = Status.from_text("""\ 382 applications: 383 dummy-sink: 384 units: 385 dummy-sink/0: 386 juju-status: 387 current: active 388 """) 389 with patch('deploy_stack.remote_from_unit', autospec=True, 390 return_value=remote): 391 with patch.object(client, 'get_status', autospec=True, 392 return_value=status): 393 check_token(client, 'token', timeout=0) 394 # application-status did not have the token, winrm did. 395 remote.cat.assert_called_once_with('%ProgramData%\\dummy-sink\\token') 396 self.assertEqual( 397 ['INFO Waiting for applications to reach ready.', 398 'INFO Retrieving token.', 399 "INFO Token matches expected 'token'"], 400 self.log_stream.getvalue().splitlines()) 401 402 def test_check_token_win_remote_failure(self): 403 env = JujuData('foo', {'type': 'azure'}) 404 client = ModelClient(env, None, None) 405 remote = MagicMock(spec=['cat', 'is_windows']) 406 remote.is_windows.return_value = True 407 error = winrm.exceptions.WinRMTransportError('a', 'oops') 408 remote.cat.side_effect = error 409 status = Status.from_text("""\ 410 applications: 411 dummy-sink: 412 units: 413 dummy-sink/0: 414 juju-status: 415 current: active 416 """) 417 with patch('deploy_stack.remote_from_unit', autospec=True, 418 return_value=remote): 419 with patch.object(client, 'get_status', autospec=True, 420 return_value=status): 421 with self.assertRaises(type(error)) as ctx: 422 check_token(client, 'token', timeout=0) 423 self.assertIs(ctx.exception, error) 424 remote.cat.assert_called_once_with('%ProgramData%\\dummy-sink\\token') 425 self.assertEqual( 426 ['INFO Waiting for applications to reach ready.', 427 'INFO Retrieving token.'], 428 self.log_stream.getvalue().splitlines()) 429 430 431 @contextmanager 432 def _fake_winrm_certs(): 433 with NamedTemporaryFile() as cert_file: 434 fake_cert = (cert_file.name, cert_file.name) 435 with patch('utility.get_winrm_certs', return_value=fake_cert): 436 yield 437 438 439 class DumpEnvLogsTestCase(FakeHomeTestCase): 440 441 log_level = logging.DEBUG 442 443 def assert_machines(self, expected, got): 444 self.assertEqual(expected, dict((k, got[k].address) for k in got)) 445 446 with _fake_winrm_certs(): 447 r0 = remote_from_address('10.10.0.1') 448 r1 = remote_from_address('10.10.0.11') 449 r2 = remote_from_address('10.10.0.22', series='win2012hvr2') 450 451 @classmethod 452 def fake_remote_machines(cls): 453 return {'0': cls.r0, '1': cls.r1, '2': cls.r2} 454 455 def test_dump_env_logs_remote(self): 456 with temp_dir() as artifacts_dir: 457 with patch('deploy_stack.get_remote_machines', autospec=True, 458 return_value=self.fake_remote_machines()) as gm_mock: 459 with patch('deploy_stack._can_run_ssh', lambda: True): 460 with patch('deploy_stack.copy_remote_logs', 461 autospec=True) as crl_mock: 462 with patch('deploy_stack.archive_logs', 463 autospec=True) as al_mock: 464 env = JujuData('foo', {'type': 'nonlocal'}) 465 client = ModelClient(env, '1.234-76', None) 466 dump_env_logs(client, '10.10.0.1', artifacts_dir) 467 al_mock.assert_called_once_with(artifacts_dir) 468 self.assertEqual( 469 ['machine-0', 'machine-1', 'machine-2'], 470 sorted(os.listdir(artifacts_dir))) 471 self.assertEqual( 472 (client, {'0': '10.10.0.1'}), gm_mock.call_args[0]) 473 self.assertItemsEqual( 474 [(self.r0, '%s/machine-0' % artifacts_dir), 475 (self.r1, '%s/machine-1' % artifacts_dir), 476 (self.r2, '%s/machine-2' % artifacts_dir)], 477 [cal[0] for cal in crl_mock.call_args_list]) 478 self.assertEqual( 479 ['INFO Retrieving logs for machine-0 using ' + repr(self.r0), 480 'INFO Retrieving logs for machine-1 using ' + repr(self.r1), 481 'INFO Retrieving logs for machine-2 using ' + repr(self.r2)], 482 self.log_stream.getvalue().splitlines()) 483 484 def test_dump_env_logs_remote_no_ssh(self): 485 with temp_dir() as artifacts_dir: 486 with patch('deploy_stack.get_remote_machines', autospec=True, 487 return_value=self.fake_remote_machines()) as gm_mock: 488 with patch('deploy_stack._can_run_ssh', lambda: False): 489 with patch('deploy_stack.copy_remote_logs', 490 autospec=True) as crl_mock: 491 with patch('deploy_stack.archive_logs', 492 autospec=True) as al_mock: 493 env = JujuData('foo', {'type': 'nonlocal'}) 494 client = ModelClient(env, '1.234-76', None) 495 dump_env_logs(client, '10.10.0.1', artifacts_dir) 496 al_mock.assert_called_once_with(artifacts_dir) 497 self.assertEqual( 498 ['machine-2'], 499 sorted(os.listdir(artifacts_dir))) 500 self.assertEqual((client, {'0': '10.10.0.1'}), gm_mock.call_args[0]) 501 self.assertEqual( 502 [(self.r2, '%s/machine-2' % artifacts_dir)], 503 [cal[0] for cal in crl_mock.call_args_list]) 504 self.assertEqual( 505 ['INFO No ssh, skipping logs for machine-0 using ' + repr(self.r0), 506 'INFO No ssh, skipping logs for machine-1 using ' + repr(self.r1), 507 'INFO Retrieving logs for machine-2 using ' + repr(self.r2)], 508 self.log_stream.getvalue().splitlines()) 509 510 def test_archive_logs(self): 511 with temp_dir() as log_dir: 512 with open(os.path.join(log_dir, 'fake.log'), 'w') as f: 513 f.write('log contents') 514 with patch('subprocess.check_call', autospec=True) as cc_mock: 515 archive_logs(log_dir) 516 log_path = os.path.join(log_dir, 'fake.log') 517 cc_mock.assert_called_once_with(['gzip', '--best', '-f', log_path]) 518 519 def test_archive_logs_syslog(self): 520 with temp_dir() as log_dir: 521 log_path = os.path.join(log_dir, 'syslog') 522 with open(log_path, 'w') as f: 523 f.write('syslog contents') 524 with patch('subprocess.check_call', autospec=True) as cc_mock: 525 archive_logs(log_dir) 526 cc_mock.assert_called_once_with(['gzip', '--best', '-f', log_path]) 527 528 def test_archive_logs_subdir(self): 529 with temp_dir() as log_dir: 530 subdir = os.path.join(log_dir, "subdir") 531 os.mkdir(subdir) 532 with open(os.path.join(subdir, 'fake.log'), 'w') as f: 533 f.write('log contents') 534 with patch('subprocess.check_call', autospec=True) as cc_mock: 535 archive_logs(log_dir) 536 log_path = os.path.join(subdir, 'fake.log') 537 cc_mock.assert_called_once_with(['gzip', '--best', '-f', log_path]) 538 539 def test_archive_logs_none(self): 540 with temp_dir() as log_dir: 541 with patch('subprocess.check_call', autospec=True) as cc_mock: 542 archive_logs(log_dir) 543 self.assertEquals(cc_mock.call_count, 0) 544 545 def test_archive_logs_multiple(self): 546 with temp_dir() as log_dir: 547 log_paths = [] 548 with open(os.path.join(log_dir, 'fake.log'), 'w') as f: 549 f.write('log contents') 550 log_paths.append(os.path.join(log_dir, 'fake.log')) 551 subdir = os.path.join(log_dir, "subdir") 552 os.mkdir(subdir) 553 with open(os.path.join(subdir, 'syslog'), 'w') as f: 554 f.write('syslog contents') 555 log_paths.append(os.path.join(subdir, 'syslog')) 556 with patch('subprocess.check_call', autospec=True) as cc_mock: 557 archive_logs(log_dir) 558 self.assertEqual(1, cc_mock.call_count) 559 call_args, call_kwargs = cc_mock.call_args 560 gzip_args = call_args[0] 561 self.assertEqual(0, len(call_kwargs)) 562 self.assertEqual(gzip_args[:3], ['gzip', '--best', '-f']) 563 self.assertEqual(set(gzip_args[3:]), set(log_paths)) 564 565 def test_copy_remote_logs(self): 566 # To get the logs, their permissions must be updated first, 567 # then downloaded in the order that they will be created 568 # to ensure errors do not prevent some logs from being retrieved. 569 with patch('deploy_stack.wait_for_port', autospec=True): 570 with patch('subprocess.check_output') as cc_mock: 571 copy_remote_logs(remote_from_address('10.10.0.1'), '/foo') 572 self.assertEqual( 573 (get_timeout_prefix(120) + ( 574 'ssh', 575 '-o', 'User ubuntu', 576 '-o', 'UserKnownHostsFile /dev/null', 577 '-o', 'StrictHostKeyChecking no', 578 '-o', 'PasswordAuthentication no', 579 '10.10.0.1', 580 'sudo chmod -Rf go+r /var/log/cloud-init*.log' 581 ' /var/log/juju/*.log' 582 ' /var/lib/juju/containers/juju-*-lxc-*/' 583 ' /var/log/lxd/juju-*' 584 ' /var/log/lxd/lxd.log' 585 ' /var/log/syslog' 586 ' /var/log/mongodb/mongodb.log' 587 ' /etc/network/interfaces' 588 ' /etc/environment' 589 ' /home/ubuntu/ifconfig.log' 590 ),), 591 cc_mock.call_args_list[0][0]) 592 self.assertEqual( 593 (get_timeout_prefix(120) + ( 594 'ssh', 595 '-o', 'User ubuntu', 596 '-o', 'UserKnownHostsFile /dev/null', 597 '-o', 'StrictHostKeyChecking no', 598 '-o', 'PasswordAuthentication no', 599 '10.10.0.1', 600 'ifconfig > /home/ubuntu/ifconfig.log'),), 601 cc_mock.call_args_list[1][0]) 602 self.assertEqual( 603 (get_timeout_prefix(120) + ( 604 'scp', '-rC', 605 '-o', 'User ubuntu', 606 '-o', 'UserKnownHostsFile /dev/null', 607 '-o', 'StrictHostKeyChecking no', 608 '-o', 'PasswordAuthentication no', 609 '10.10.0.1:/var/log/cloud-init*.log', 610 '10.10.0.1:/var/log/juju/*.log', 611 '10.10.0.1:/var/lib/juju/containers/juju-*-lxc-*/', 612 '10.10.0.1:/var/log/lxd/juju-*', 613 '10.10.0.1:/var/log/lxd/lxd.log', 614 '10.10.0.1:/var/log/syslog', 615 '10.10.0.1:/var/log/mongodb/mongodb.log', 616 '10.10.0.1:/etc/network/interfaces', 617 '10.10.0.1:/etc/environment', 618 '10.10.0.1:/home/ubuntu/ifconfig.log', 619 '/foo'),), 620 cc_mock.call_args_list[2][0]) 621 622 def test_copy_remote_logs_windows(self): 623 with _fake_winrm_certs(): 624 remote = remote_from_address('10.10.0.1', series="win2012hvr2") 625 with patch.object(remote, "copy", autospec=True) as copy_mock: 626 copy_remote_logs(remote, '/foo') 627 paths = [ 628 "%ProgramFiles(x86)%\\Cloudbase Solutions\\Cloudbase-Init\\log\\*", 629 "C:\\Juju\\log\\juju\\*.log", 630 ] 631 copy_mock.assert_called_once_with("/foo", paths) 632 633 def test_copy_remote_logs_with_errors(self): 634 # Ssh and scp errors will happen when /var/log/juju doesn't exist yet, 635 # but we log the case anc continue to retrieve as much as we can. 636 def remote_op(*args, **kwargs): 637 if 'ssh' in args: 638 raise subprocess.CalledProcessError('ssh error', 'output') 639 else: 640 raise subprocess.CalledProcessError('scp error', 'output') 641 642 with patch('subprocess.check_output', side_effect=remote_op) as co: 643 with patch('deploy_stack.wait_for_port', autospec=True): 644 copy_remote_logs(remote_from_address('10.10.0.1'), '/foo') 645 self.assertEqual(3, co.call_count) 646 self.assertEqual( 647 ["DEBUG ssh -o 'User ubuntu' -o 'UserKnownHostsFile /dev/null' " 648 "-o 'StrictHostKeyChecking no' -o 'PasswordAuthentication no' " 649 "10.10.0.1 'sudo chmod -Rf go+r /var/log/cloud-init*.log " 650 "/var/log/juju/*.log /var/lib/juju/containers/juju-*-lxc-*/ " 651 "/var/log/lxd/juju-* " 652 "/var/log/lxd/lxd.log " 653 "/var/log/syslog " 654 "/var/log/mongodb/mongodb.log " 655 "/etc/network/interfaces " 656 "/etc/environment " 657 "/home/ubuntu/ifconfig.log'", 658 'WARNING Could not allow access to the juju logs:', 659 'WARNING None', 660 "DEBUG ssh -o 'User ubuntu' -o 'UserKnownHostsFile /dev/null' " 661 "-o 'StrictHostKeyChecking no' -o 'PasswordAuthentication no' " 662 "10.10.0.1 'ifconfig > /home/ubuntu/ifconfig.log'", 663 'WARNING Could not capture ifconfig state:', 664 'WARNING None', 'WARNING Could not retrieve some or all logs:', 665 'WARNING CalledProcessError()', 666 ], 667 self.log_stream.getvalue().splitlines()) 668 669 def test_get_machines_for_logs(self): 670 client = ModelClient( 671 JujuData('cloud', {'type': 'ec2'}), '1.23.4', None) 672 status = Status.from_text("""\ 673 machines: 674 "0": 675 dns-name: 10.11.12.13 676 "1": 677 dns-name: 10.11.12.14 678 """) 679 with patch.object(client, 'get_status', autospec=True, 680 return_value=status): 681 machines = get_remote_machines(client, {}) 682 self.assert_machines( 683 {'0': '10.11.12.13', '1': '10.11.12.14'}, machines) 684 685 def test_get_machines_for_logs_with_bootstrap_host(self): 686 client = ModelClient( 687 JujuData('cloud', {'type': 'ec2'}), '1.23.4', None) 688 status = Status.from_text("""\ 689 machines: 690 "0": 691 instance-id: pending 692 """) 693 with patch.object(client, 'get_status', autospec=True, 694 return_value=status): 695 machines = get_remote_machines(client, {'0': '10.11.111.222'}) 696 self.assert_machines({'0': '10.11.111.222'}, machines) 697 698 def test_get_machines_for_logs_with_no_addresses(self): 699 client = ModelClient( 700 JujuData('cloud', {'type': 'ec2'}), '1.23.4', None) 701 with patch.object(client, 'get_status', autospec=True, 702 side_effect=Exception): 703 machines = get_remote_machines(client, {'0': '10.11.111.222'}) 704 self.assert_machines({'0': '10.11.111.222'}, machines) 705 706 @patch('subprocess.check_call') 707 def test_get_remote_machines_with_maas(self, cc_mock): 708 config = { 709 'type': 'maas', 710 'name': 'foo', 711 'maas-server': 'http://bar/MASS/', 712 } 713 juju_data = JujuData('cloud', config) 714 cloud_name = 'mycloud' 715 juju_data.clouds = {'clouds': {cloud_name: { 716 'endpoint': config['maas-server'], 717 'type': config['type'], 718 }}} 719 juju_data.credentials = {'credentials': {cloud_name: {'credentials': { 720 'maas-oauth': 'baz', 721 }}}} 722 client = ModelClient(juju_data, '1.23.4', None) 723 status = Status.from_text("""\ 724 machines: 725 "0": 726 dns-name: node1.maas 727 "1": 728 dns-name: node2.maas 729 """) 730 with patch.object(client, 'get_status', autospec=True, 731 return_value=status): 732 allocated_ips = { 733 'node1.maas': '10.11.12.13', 734 'node2.maas': '10.11.12.14', 735 } 736 with patch('substrate.MAASAccount.get_allocated_ips', 737 autospec=True, return_value=allocated_ips): 738 machines = get_remote_machines(client, {'0': 'node1.maas'}) 739 self.assert_machines( 740 {'0': '10.11.12.13', '1': '10.11.12.14'}, machines) 741 742 def test_iter_remote_machines(self): 743 client = ModelClient( 744 JujuData('cloud', {'type': 'ec2'}), '1.23.4', None) 745 status = Status.from_text("""\ 746 machines: 747 "0": 748 dns-name: 10.11.12.13 749 "1": 750 dns-name: 10.11.12.14 751 """) 752 with patch.object(client, 'get_status', autospec=True, 753 return_value=status): 754 machines = [(m, r.address) 755 for m, r in iter_remote_machines(client)] 756 self.assertEqual( 757 [('0', '10.11.12.13'), ('1', '10.11.12.14')], machines) 758 759 def test_iter_remote_machines_with_series(self): 760 client = ModelClient( 761 JujuData('cloud', {'type': 'ec2'}), '1.23.4', None) 762 status = Status.from_text("""\ 763 machines: 764 "0": 765 dns-name: 10.11.12.13 766 series: trusty 767 "1": 768 dns-name: 10.11.12.14 769 series: win2012hvr2 770 """) 771 with patch.object(client, 'get_status', autospec=True, 772 return_value=status): 773 with _fake_winrm_certs(): 774 machines = [(m, r.address, r.series) 775 for m, r in iter_remote_machines(client)] 776 self.assertEqual( 777 [('0', '10.11.12.13', 'trusty'), 778 ('1', '10.11.12.14', 'win2012hvr2')], machines) 779 780 781 class TestDeployDummyStack(FakeHomeTestCase): 782 783 def test_deploy_dummy_stack_sets_centos_constraints(self): 784 env = JujuData('foo', {'type': 'maas'}) 785 client = ModelClient(env, '2.0.0', '/foo/juju') 786 with patch('subprocess.check_call', autospec=True) as cc_mock: 787 with patch.object(ModelClient, 'wait_for_started'): 788 with patch('deploy_stack.get_random_string', 789 return_value='fake-token', autospec=True): 790 deploy_dummy_stack(client, 'centos') 791 assert_juju_call(self, cc_mock, client, 792 ('juju', '--show-log', 'set-model-constraints', '-m', 793 'foo:foo', 'tags=MAAS_NIC_1'), 0) 794 795 def test_assess_juju_relations(self): 796 env = JujuData('foo', {'type': 'nonlocal'}) 797 client = ModelClient(env, None, '/foo/juju') 798 with patch.object(client, 'get_juju_output', side_effect='fake-token', 799 autospec=True): 800 with patch('subprocess.check_call', autospec=True) as cc_mock: 801 with patch('deploy_stack.get_random_string', 802 return_value='fake-token', autospec=True): 803 with patch('deploy_stack.check_token', 804 autospec=True) as ct_mock: 805 assess_juju_relations(client) 806 assert_juju_call(self, cc_mock, client, ( 807 'juju', '--show-log', 'config', '-m', 'foo:foo', 808 'dummy-source', 'token=fake-token'), 0) 809 ct_mock.assert_called_once_with(client, 'fake-token') 810 811 def test_deploy_dummy_stack_centos(self): 812 client = fake_juju_client() 813 client.bootstrap() 814 with patch.object(client, 'deploy', autospec=True) as dp_mock: 815 with temp_os_env('JUJU_REPOSITORY', '/tmp/repo'): 816 deploy_dummy_stack(client, 'centos7') 817 calls = [ 818 call('/tmp/repo/charms-centos/dummy-source', series='centos7'), 819 call('/tmp/repo/charms-centos/dummy-sink', series='centos7')] 820 self.assertEqual(dp_mock.mock_calls, calls) 821 822 def test_deploy_dummy_stack_win(self): 823 client = fake_juju_client() 824 client.bootstrap() 825 with patch.object(client, 'deploy', autospec=True) as dp_mock: 826 with temp_os_env('JUJU_REPOSITORY', '/tmp/repo'): 827 deploy_dummy_stack(client, 'win2012hvr2') 828 calls = [ 829 call('/tmp/repo/charms-win/dummy-source', series='win2012hvr2'), 830 call('/tmp/repo/charms-win/dummy-sink', series='win2012hvr2')] 831 self.assertEqual(dp_mock.mock_calls, calls) 832 833 def test_deploy_dummy_stack_charmstore(self): 834 client = fake_juju_client() 835 client.bootstrap() 836 with patch.object(client, 'deploy', autospec=True) as dp_mock: 837 deploy_dummy_stack(client, 'bionic', use_charmstore=True) 838 calls = [ 839 call('cs:~juju-qa/dummy-source', series='bionic'), 840 call('cs:~juju-qa/dummy-sink', series='bionic')] 841 self.assertEqual(dp_mock.mock_calls, calls) 842 843 def test_deploy_dummy_stack(self): 844 env = JujuData('foo', {'type': 'nonlocal'}) 845 client = ModelClient(env, '2.0.0', '/foo/juju') 846 status = yaml.safe_dump({ 847 'machines': {'0': {'agent-state': 'started'}}, 848 'services': { 849 'dummy-sink': {'units': { 850 'dummy-sink/0': {'agent-state': 'started'}} 851 } 852 } 853 }) 854 855 def output(*args, **kwargs): 856 token_file = '/var/run/dummy-sink/token' 857 output = { 858 ('status', '--format', 'yaml'): status, 859 ('ssh', 'dummy-sink/0', 'cat', token_file): 'fake-token', 860 } 861 return output[args] 862 863 with patch.object(client, 'get_juju_output', side_effect=output, 864 autospec=True) as gjo_mock: 865 with patch('subprocess.check_call', autospec=True) as cc_mock: 866 with patch('deploy_stack.get_random_string', 867 return_value='fake-token', autospec=True): 868 with patch('sys.stdout', autospec=True): 869 with temp_os_env('JUJU_REPOSITORY', '/tmp/repo'): 870 deploy_dummy_stack(client, 'bar-') 871 assert_juju_call(self, cc_mock, client, ( 872 'juju', '--show-log', 'deploy', '-m', 'foo:foo', 873 '/tmp/repo/charms/dummy-source', '--series', 'bar-'), 0) 874 assert_juju_call(self, cc_mock, client, ( 875 'juju', '--show-log', 'deploy', '-m', 'foo:foo', 876 '/tmp/repo/charms/dummy-sink', '--series', 'bar-'), 1) 877 assert_juju_call(self, cc_mock, client, ( 878 'juju', '--show-log', 'integrate', '-m', 'foo:foo', 879 'dummy-source', 'dummy-sink'), 2) 880 assert_juju_call(self, cc_mock, client, ( 881 'juju', '--show-log', 'expose', '-m', 'foo:foo', 'dummy-sink'), 3) 882 self.assertEqual(cc_mock.call_count, 4) 883 self.assertEqual( 884 [ 885 call('status', '--format', 'yaml', controller=False) 886 ], 887 gjo_mock.call_args_list) 888 889 client = client.clone(version='1.25.0') 890 with patch.object(client, 'get_juju_output', side_effect=output, 891 autospec=True) as gjo_mock: 892 with patch('subprocess.check_call', autospec=True) as cc_mock: 893 with patch('deploy_stack.get_random_string', 894 return_value='fake-token', autospec=True): 895 with patch('sys.stdout', autospec=True): 896 with temp_os_env('JUJU_REPOSITORY', '/tmp/repo'): 897 deploy_dummy_stack(client, 'bar-') 898 assert_juju_call(self, cc_mock, client, ( 899 'juju', '--show-log', 'deploy', '-m', 'foo:foo', 900 'local:bar-/dummy-source', '--series', 'bar-'), 0) 901 assert_juju_call(self, cc_mock, client, ( 902 'juju', '--show-log', 'deploy', '-m', 'foo:foo', 903 'local:bar-/dummy-sink', '--series', 'bar-'), 1) 904 905 906 def fake_ModelClient(env, path=None, debug=None): 907 return ModelClient(env=env, version='1.2.3.4', full_path=path) 908 909 910 class FakeBootstrapManager: 911 912 def __init__(self, client, keep_env=False): 913 self.client = client 914 self.tear_down_client = client 915 self.entered_top = False 916 self.exited_top = False 917 self.entered_bootstrap = False 918 self.exited_bootstrap = False 919 self.entered_runtime = False 920 self.exited_runtime = False 921 self.torn_down = False 922 self.known_hosts = {'0': '0.example.org'} 923 self.keep_env = keep_env 924 925 @contextmanager 926 def top_context(self): 927 try: 928 self.entered_top = True 929 yield ['bar'] 930 finally: 931 self.exited_top = True 932 933 @contextmanager 934 def bootstrap_context(self, machines): 935 initial_home = self.client.env.juju_home 936 self.client.env.environment = self.client.env.environment + '-temp' 937 self.client.env.controller.name = self.client.env.environment 938 try: 939 self.entered_bootstrap = True 940 self.client.env.juju_home = os.path.join(initial_home, 'isolated') 941 self.client.bootstrap() 942 yield 943 finally: 944 self.exited_bootstrap = True 945 if not self.permanent: 946 self.client.env.juju_home = initial_home 947 948 @contextmanager 949 def runtime_context(self, machines): 950 try: 951 self.entered_runtime = True 952 yield 953 finally: 954 if not self.keep_env: 955 self.tear_down() 956 self.exited_runtime = True 957 958 def tear_down(self): 959 self.tear_down_client.kill_controller() 960 self.torn_down = True 961 962 @contextmanager 963 def booted_context(self, upload_tools, **kwargs): 964 with self.top_context() as machines: 965 with self.bootstrap_context(machines): 966 self.client.bootstrap(upload_tools) 967 with self.runtime_context(machines): 968 yield machines 969 970 971 class TestDeployJob(FakeHomeTestCase): 972 973 @contextmanager 974 def ds_cxt(self): 975 env = JujuData('foo', {}) 976 client = fake_ModelClient(env) 977 bc_cxt = patch('deploy_stack.client_from_config', 978 return_value=client) 979 fc_cxt = patch('jujupy.JujuData.from_config', 980 return_value=env) 981 mgr = MagicMock() 982 bm_cxt = patch('deploy_stack.BootstrapManager', autospec=True, 983 return_value=mgr) 984 juju_cxt = patch('jujupy.ModelClient.juju', autospec=True) 985 ajr_cxt = patch('deploy_stack.assess_juju_exec', autospec=True) 986 dds_cxt = patch('deploy_stack.deploy_dummy_stack', autospec=True) 987 with bc_cxt, fc_cxt, bm_cxt as bm_mock, juju_cxt, ajr_cxt, dds_cxt: 988 yield client, bm_mock 989 990 def test_region(self): 991 args = Namespace( 992 env='base', juju_bin='/fake/juju', logs='log', temp_env_name='foo', 993 charm_prefix=None, bootstrap_host=None, machine=None, 994 series='trusty', debug=False, agent_url=None, agent_stream=None, 995 keep_env=False, upload_tools=False, 996 region='region-foo', verbose=False, upgrade=False, deadline=None, 997 controller_host=None, use_charmstore=False, to=None) 998 with self.ds_cxt() as (client, bm_mock): 999 with patch('deploy_stack.assess_juju_relations', 1000 autospec=True): 1001 with patch('subprocess.Popen', autospec=True, 1002 return_value=FakePopen('', '', 0)): 1003 with patch('deploy_stack.make_controller_strategy', 1004 ) as mcs_mock: 1005 _deploy_job(args, 'local:trusty/', 'trusty') 1006 bm_mock.assert_called_once_with( 1007 'foo', client, client, None, None, 'trusty', None, None, 1008 'region-foo', 'log', False, 1009 controller_strategy=mcs_mock.return_value) 1010 1011 def test_deploy_job_changes_series_with_win(self): 1012 args = Namespace( 1013 series='windows', temp_env_name='windows', env=None, upgrade=None, 1014 charm_prefix=None, bootstrap_host=None, machine=None, logs=None, 1015 debug=None, juju_bin=None, agent_url=None, agent_stream=None, 1016 keep_env=None, upload_tools=None, 1017 region=None, verbose=None, use_charmstore=False, to=None) 1018 with patch('deploy_stack.deploy_job_parse_args', return_value=args, 1019 autospec=True): 1020 with patch('deploy_stack._deploy_job', autospec=True) as ds_mock: 1021 deploy_job() 1022 ds_mock.assert_called_once_with(args, 'windows', 'trusty') 1023 1024 def test_deploy_job_changes_series_with_centos(self): 1025 args = Namespace( 1026 series='centos', temp_env_name='centos', env=None, upgrade=None, 1027 charm_prefix=None, bootstrap_host=None, machine=None, logs=None, 1028 debug=None, juju_bin=None, agent_url=None, agent_stream=None, 1029 keep_env=None, upload_tools=None, 1030 region=None, verbose=None, use_charmstore=False, to=None) 1031 with patch('deploy_stack.deploy_job_parse_args', return_value=args, 1032 autospec=True): 1033 with patch('deploy_stack._deploy_job', autospec=True) as ds_mock: 1034 deploy_job() 1035 ds_mock.assert_called_once_with(args, 'centos', 'trusty') 1036 1037 1038 class TestTestUpgrade(FakeHomeTestCase): 1039 1040 RUN_UNAME = ( 1041 'juju', '--show-log', 'exec', '--format', 'json', 1042 '--application', 'dummy-source,dummy-sink', 'uname') 1043 STATUS = ( 1044 'juju', '--show-log', 'status', '-m', 'foo:foo', 1045 '--format', 'yaml') 1046 CONTROLLER_STATUS = ( 1047 'juju', '--show-log', 'status', '-m', 'foo:controller', 1048 '--format', 'yaml') 1049 GET_ENV = ('juju', '--show-log', 'model-config', '-m', 'foo:foo', 1050 'agent-metadata-url') 1051 GET_CONTROLLER_ENV = ( 1052 'juju', '--show-log', 'model-config', '-m', 'foo:controller', 1053 'agent-metadata-url') 1054 LIST_MODELS = ( 1055 'juju', '--show-log', 'list-models', '-c', 'foo', '--format', 'yaml') 1056 1057 @classmethod 1058 def upgrade_output(cls, args, **kwargs): 1059 status = yaml.safe_dump({ 1060 'machines': {'0': { 1061 'agent-state': 'started', 1062 'agent-version': '2.0-rc2'}}, 1063 'services': {}}) 1064 juju_run_out = json.dumps( 1065 [{"ubuntu/0":{"id":"9","results":{"return-code":0,"stdout":"Linux\n"},"status":"completed","timing":{"completed":"2021-01-04 20:12:11 +0000 UTC","enqueued":"2021-01-04 20:12:07 +0000 UTC","started":"2021-01-04 20:12:10 +0000 UTC"},"unit":"ubuntu/0"}, 1066 "wordpress/0":{"id":"10","results":{"return-code":0,"stdout":"Linux\n"},"status":"completed","timing":{"completed":"2021-01-04 20:12:10 +0000 UTC","enqueued":"2021-01-04 20:12:07 +0000 UTC","started":"2021-01-04 20:12:10 +0000 UTC"},"unit":"wordpress/0"}}]) 1067 list_models = json.dumps( 1068 {'models': [ 1069 {'name': 'controller'}, 1070 {'name': 'foo'}, 1071 ]}) 1072 output = { 1073 cls.STATUS: status, 1074 cls.CONTROLLER_STATUS: status, 1075 cls.RUN_UNAME: juju_run_out, 1076 cls.GET_ENV: 'testing', 1077 cls.GET_CONTROLLER_ENV: 'testing', 1078 cls.LIST_MODELS: list_models, 1079 } 1080 return FakePopen(output[args], '', 0) 1081 1082 @contextmanager 1083 def upgrade_mocks(self): 1084 with patch('subprocess.Popen', side_effect=self.upgrade_output, 1085 autospec=True) as co_mock: 1086 with patch('subprocess.check_call', autospec=True) as cc_mock: 1087 with patch('deploy_stack.check_token', autospec=True): 1088 with patch('deploy_stack.get_random_string', 1089 return_value="FAKETOKEN", autospec=True): 1090 with patch('jujupy.ModelClient.get_version', 1091 side_effect=lambda cls: 1092 '2.0-rc2-arch-series'): 1093 with patch( 1094 'jujupy.backend.get_timeout_prefix', 1095 autospec=True, return_value=()): 1096 yield (co_mock, cc_mock) 1097 1098 def test_assess_upgrade(self): 1099 env = JujuData('foo', {'type': 'foo'}) 1100 old_client = ModelClient(env, None, '/foo/juju') 1101 with self.upgrade_mocks() as (co_mock, cc_mock): 1102 assess_upgrade(old_client, '/bar/juju') 1103 new_client = ModelClient(env, None, '/bar/juju') 1104 # Needs to upgrade the controller first. 1105 assert_juju_call(self, cc_mock, new_client, ( 1106 'juju', '--show-log', 'upgrade-juju', '-m', 'foo:controller', 1107 '--agent-version', '2.0-rc2'), 0) 1108 assert_juju_call(self, cc_mock, new_client, ( 1109 'juju', '--show-log', 'status', '-m', 'foo:controller', 1110 '--format', 'yaml'), 1) 1111 assert_juju_call(self, cc_mock, new_client, ( 1112 'juju', '--show-log', 'list-models', '-c', 'foo'), 2) 1113 assert_juju_call(self, cc_mock, new_client, ( 1114 'juju', '--show-log', 'upgrade-juju', '-m', 'foo:foo', 1115 '--agent-version', '2.0-rc2'), 3) 1116 self.assertEqual(cc_mock.call_count, 6) 1117 assert_juju_call(self, co_mock, new_client, self.LIST_MODELS, 0) 1118 assert_juju_call(self, co_mock, new_client, self.GET_CONTROLLER_ENV, 1) 1119 assert_juju_call(self, co_mock, new_client, self.GET_CONTROLLER_ENV, 2) 1120 assert_juju_call(self, co_mock, new_client, self.CONTROLLER_STATUS, 3) 1121 assert_juju_call(self, co_mock, new_client, self.GET_ENV, 5) 1122 assert_juju_call(self, co_mock, new_client, self.GET_ENV, 6) 1123 assert_juju_call(self, co_mock, new_client, self.STATUS, 7) 1124 self.assertEqual(co_mock.call_count, 9) 1125 1126 def test__get_clients_to_upgrade_returns_controller_and_model(self): 1127 old_client = fake_juju_client() 1128 old_client.bootstrap() 1129 1130 with patch('jujupy.ModelClient.get_version', 1131 return_value='2.0-rc2-arch-series'): 1132 new_clients = _get_clients_to_upgrade( 1133 old_client, '/foo/newer/juju') 1134 1135 self.assertEqual(len(new_clients), 2) 1136 self.assertEqual(new_clients[0].model_name, 'controller') 1137 self.assertEqual(new_clients[1].model_name, 'name') 1138 1139 def test_mass_timeout(self): 1140 config = {'type': 'foo'} 1141 old_client = ModelClient(JujuData('foo', config), None, '/foo/juju') 1142 with self.upgrade_mocks(): 1143 with patch.object(ModelClient, 'wait_for_version') as wfv_mock: 1144 assess_upgrade(old_client, '/bar/juju') 1145 wfv_mock.assert_has_calls([call('2.0-rc2', 600)] * 2) 1146 config['type'] = 'maas' 1147 with patch.object(ModelClient, 'wait_for_version') as wfv_mock: 1148 assess_upgrade(old_client, '/bar/juju') 1149 wfv_mock.assert_has_calls([call('2.0-rc2', 1200)] * 2) 1150 1151 1152 class TestMakeControllerStrategy(TestCase): 1153 1154 def test_make_controller_strategy_no_host(self): 1155 client = object() 1156 tear_down_client = object() 1157 strategy = make_controller_strategy(client, tear_down_client, None) 1158 self.assertIs(CreateController, type(strategy)) 1159 self.assertEqual(client, strategy.client) 1160 self.assertEqual(tear_down_client, strategy.tear_down_client) 1161 1162 def test_make_controller_strategy_host(self): 1163 client = object() 1164 tear_down_client = object() 1165 with patch.dict(os.environ, { 1166 'SSO_EMAIL': 'sso@email', 1167 'SSO_PASSWORD': 'sso-password'}): 1168 strategy = make_controller_strategy(client, tear_down_client, 1169 'host') 1170 self.assertIs(PublicController, type(strategy)) 1171 self.assertEqual(client, strategy.client) 1172 self.assertEqual(tear_down_client, strategy.tear_down_client) 1173 self.assertEqual('sso@email', strategy.email) 1174 self.assertEqual('sso-password', strategy.password) 1175 1176 1177 class TestExistingController(TestCase): 1178 1179 def get_controller(self): 1180 client = fake_juju_client() 1181 create_controller = ExistingController(client) 1182 return create_controller 1183 1184 def test_prepare(self): 1185 controller = self.get_controller() 1186 controller.prepare('foo') 1187 self.assertEqual('foo', controller.client.env.controller.name) 1188 1189 def test_create_initial_model(self): 1190 controller = self.get_controller() 1191 client = controller.client 1192 self.assertEqual({'models': []}, client.get_models()) 1193 controller.create_initial_model() 1194 self.assertEqual([{'name': 'name'}], client.get_models()['models']) 1195 1196 def test_tear_down(self): 1197 controller = self.get_controller() 1198 client = controller.client 1199 client.add_model(client.env) 1200 self.assertEqual([{'name': 'name'}], client.get_models()['models']) 1201 controller.tear_down('') 1202 self.assertEqual([], client.get_models()['models']) 1203 1204 1205 class TestCreateController(FakeHomeTestCase): 1206 1207 def get_cleanup_controller(self): 1208 client = fake_juju_client() 1209 create_controller = CreateController(None, client) 1210 return create_controller 1211 1212 def get_controller(self): 1213 client = fake_juju_client() 1214 create_controller = CreateController(client, None) 1215 return create_controller 1216 1217 def test_prepare_no_existing(self): 1218 create_controller = self.get_cleanup_controller() 1219 client = create_controller.tear_down_client 1220 create_controller.prepare() 1221 self.assertEqual({'models': []}, client.get_models()) 1222 self.assertEqual( 1223 'not-bootstrapped', client._backend.controller_state.state) 1224 1225 def test_prepare_leftover(self): 1226 create_controller = self.get_cleanup_controller() 1227 client = create_controller.tear_down_client 1228 client.bootstrap() 1229 create_controller.prepare() 1230 self.assertEqual({'models': []}, client.get_models()) 1231 self.assertEqual( 1232 'controller-killed', client._backend.controller_state.state) 1233 1234 def test_create_initial_model(self): 1235 controller = self.get_controller() 1236 client = controller.client 1237 self.assertEqual({'models': []}, client.get_models()) 1238 controller.create_initial_model(False, 'angsty', {}) 1239 self.assertItemsEqual([{'name': 'controller'}, {'name': 'name'}], 1240 client.get_models()['models']) 1241 self.assertEqual( 1242 'bootstrapped', client._backend.controller_state.state) 1243 1244 def test_get_hosts(self): 1245 controller = self.get_controller() 1246 client = controller.client 1247 client.bootstrap() 1248 self.assertEqual({'0': '0.example.com'}, controller.get_hosts()) 1249 1250 def test_tear_down_existing(self): 1251 create_controller = self.get_cleanup_controller() 1252 client = create_controller.tear_down_client 1253 client.bootstrap() 1254 create_controller.tear_down(True) 1255 self.assertEqual({'models': []}, client.get_models()) 1256 self.assertEqual( 1257 'controller-destroyed', client._backend.controller_state.state) 1258 1259 def test_tear_down_existing_no_controller(self): 1260 create_controller = self.get_cleanup_controller() 1261 client = create_controller.tear_down_client 1262 client.bootstrap() 1263 create_controller.tear_down(False) 1264 self.assertEqual({'models': []}, client.get_models()) 1265 self.assertEqual( 1266 'controller-killed', client._backend.controller_state.state) 1267 1268 def test_tear_down_nothing(self): 1269 create_controller = self.get_cleanup_controller() 1270 with self.assertRaises(subprocess.CalledProcessError): 1271 create_controller.tear_down(True) 1272 1273 1274 class TestPublicController(FakeHomeTestCase): 1275 1276 def get_cleanup_controller(self): 1277 client = fake_juju_client() 1278 public_controller = PublicController('host', 'email2', 'password2', 1279 None, client) 1280 return public_controller 1281 1282 def get_controller(self): 1283 client = fake_juju_client() 1284 public_controller = PublicController('host', 'email2', 'password2', 1285 client, None) 1286 return public_controller 1287 1288 def test_prepare_no_existing(self): 1289 public_controller = self.get_cleanup_controller() 1290 client = public_controller.tear_down_client 1291 public_controller.prepare() 1292 self.assertEqual({'models': []}, client.get_models()) 1293 self.assertEqual( 1294 'not-bootstrapped', client._backend.controller_state.state) 1295 1296 def test_prepare_leftover(self): 1297 public_controller = self.get_cleanup_controller() 1298 client = public_controller.tear_down_client 1299 client.add_model(client.env) 1300 public_controller.prepare() 1301 self.assertEqual({'models': []}, client.get_models()) 1302 self.assertEqual( 1303 'model-destroyed', client._backend.controller_state.state) 1304 1305 def test_create_initial_model(self): 1306 controller = self.get_controller() 1307 client = controller.client 1308 self.assertEqual({'models': []}, client.get_models()) 1309 controller.create_initial_model(False, 'angsty', {}) 1310 self.assertItemsEqual([{'name': 'name'}], 1311 client.get_models()['models']) 1312 self.assertEqual( 1313 'created', client._backend.controller_state.state) 1314 1315 def test_get_hosts(self): 1316 controller = self.get_controller() 1317 client = controller.client 1318 client.bootstrap() 1319 self.assertEqual({}, controller.get_hosts()) 1320 1321 def test_tear_down_existing(self): 1322 public_controller = self.get_cleanup_controller() 1323 client = public_controller.tear_down_client 1324 client.add_model(client.env) 1325 public_controller.tear_down(True) 1326 self.assertEqual({'models': []}, client.get_models()) 1327 self.assertEqual( 1328 'model-destroyed', client._backend.controller_state.state) 1329 1330 def test_tear_down_nothing(self): 1331 public_controller = self.get_cleanup_controller() 1332 with self.assertRaises(subprocess.CalledProcessError): 1333 public_controller.tear_down(True) 1334 1335 1336 class TestBootstrapManager(FakeHomeTestCase): 1337 1338 def test_from_args(self): 1339 deadline = datetime(2012, 11, 10, 9, 8, 7) 1340 args = Namespace( 1341 env='foo', juju_bin='bar', debug=True, temp_env_name='baz', 1342 bootstrap_host='example.org', machine=['example.com'], 1343 series='angsty', agent_url='qux', agent_stream='escaped', 1344 region='eu-west-northwest-5', logs='pine', keep_env=True, 1345 deadline=deadline, to=None) 1346 with patch('deploy_stack.client_from_config') as fc_mock: 1347 bs_manager = BootstrapManager.from_args(args) 1348 fc_mock.assert_called_once_with('foo', 'bar', debug=True, 1349 soft_deadline=deadline) 1350 self.assertEqual('baz', bs_manager.temp_env_name) 1351 self.assertIs(fc_mock.return_value, bs_manager.client) 1352 self.assertIs(fc_mock.return_value, bs_manager.tear_down_client) 1353 self.assertEqual('example.org', bs_manager.bootstrap_host) 1354 self.assertEqual(['example.com'], bs_manager.machines) 1355 self.assertEqual('angsty', bs_manager.series) 1356 self.assertEqual('qux', bs_manager.agent_url) 1357 self.assertEqual('escaped', bs_manager.agent_stream) 1358 self.assertEqual('eu-west-northwest-5', bs_manager.region) 1359 self.assertIs(True, bs_manager.keep_env) 1360 self.assertEqual('pine', bs_manager.log_dir) 1361 self.assertEqual({'0': 'example.org'}, bs_manager.known_hosts) 1362 self.assertIsFalse(bs_manager.has_controller) 1363 1364 def test_from_existing(self): 1365 deadline = datetime(2012, 11, 10, 9, 8, 7) 1366 args = Namespace( 1367 env='foo', juju_bin='bar', debug=True, temp_env_name='baz', 1368 bootstrap_host='example.org', machine=['example.com'], 1369 series='angsty', agent_url='qux', agent_stream='escaped', 1370 region='eu-west-northwest-5', logs='pine', keep_env=True, 1371 deadline=deadline, to=None, existing='existing') 1372 with patch('deploy_stack.client_for_existing') as fc_mock: 1373 with patch.dict('os.environ', {'JUJU_DATA': 'foo'}): 1374 with patch('deploy_stack.os.path.isdir'): 1375 bs_manager = BootstrapManager._for_existing_controller( 1376 args) 1377 fc_mock.assert_called_once_with( 1378 'bar', 'foo', controller_name='existing', model_name='baz') 1379 self.assertEqual('baz', bs_manager.temp_env_name) 1380 self.assertIs(fc_mock.return_value, bs_manager.client) 1381 self.assertIs(fc_mock.return_value, bs_manager.tear_down_client) 1382 self.assertEqual('example.org', bs_manager.bootstrap_host) 1383 self.assertEqual(['example.com'], bs_manager.machines) 1384 self.assertEqual('angsty', bs_manager.series) 1385 self.assertEqual('qux', bs_manager.agent_url) 1386 self.assertEqual('escaped', bs_manager.agent_stream) 1387 self.assertEqual('eu-west-northwest-5', bs_manager.region) 1388 self.assertIs(True, bs_manager.keep_env) 1389 self.assertEqual('pine', bs_manager.log_dir) 1390 self.assertEqual({'0': 'example.org'}, bs_manager.known_hosts) 1391 self.assertIsFalse(bs_manager.has_controller) 1392 1393 def test_from_client(self): 1394 deadline = datetime(2012, 11, 10, 9, 8, 7) 1395 args = Namespace( 1396 env='foo', juju_bin='bar', debug=True, temp_env_name='baz', 1397 bootstrap_host='example.org', machine=['example.com'], 1398 series='angsty', agent_url='qux', agent_stream='escaped', 1399 region='eu-west-northwest-5', logs='pine', keep_env=True, 1400 deadline=deadline, to=None) 1401 client = fake_juju_client() 1402 bs_manager = BootstrapManager.from_client(args, client) 1403 self.assertEqual('baz', bs_manager.temp_env_name) 1404 self.assertIs(client, bs_manager.client) 1405 self.assertEqual('example.org', bs_manager.bootstrap_host) 1406 self.assertEqual(['example.com'], bs_manager.machines) 1407 self.assertEqual('angsty', bs_manager.series) 1408 self.assertEqual('qux', bs_manager.agent_url) 1409 self.assertEqual('escaped', bs_manager.agent_stream) 1410 self.assertEqual('eu-west-northwest-5', bs_manager.region) 1411 self.assertIs(True, bs_manager.keep_env) 1412 self.assertEqual('pine', bs_manager.log_dir) 1413 self.assertEqual({'0': 'example.org'}, bs_manager.known_hosts) 1414 self.assertIsFalse(bs_manager.has_controller) 1415 1416 def test_no_args(self): 1417 args = Namespace( 1418 env='foo', juju_bin='bar', debug=True, temp_env_name='baz', 1419 bootstrap_host='example.org', machine=['example.com'], 1420 series='angsty', agent_url='qux', agent_stream='escaped', 1421 region='eu-west-northwest-5', logs=None, keep_env=True, 1422 deadline=None, to=None) 1423 with patch('deploy_stack.client_from_config') as fc_mock: 1424 with patch('utility.os.makedirs'): 1425 bs_manager = BootstrapManager.from_args(args) 1426 fc_mock.assert_called_once_with('foo', 'bar', debug=True, 1427 soft_deadline=None) 1428 self.assertEqual('baz', bs_manager.temp_env_name) 1429 self.assertIs(fc_mock.return_value, bs_manager.client) 1430 self.assertIs(fc_mock.return_value, bs_manager.tear_down_client) 1431 self.assertEqual('example.org', bs_manager.bootstrap_host) 1432 self.assertEqual(['example.com'], bs_manager.machines) 1433 self.assertEqual('angsty', bs_manager.series) 1434 self.assertEqual('qux', bs_manager.agent_url) 1435 self.assertEqual('escaped', bs_manager.agent_stream) 1436 self.assertEqual('eu-west-northwest-5', bs_manager.region) 1437 self.assertIs(True, bs_manager.keep_env) 1438 logs_arg = bs_manager.log_dir.split("/") 1439 logs_ts = logs_arg[4] 1440 self.assertEqual(logs_arg[1:4], ['tmp', 'baz', 'logs']) 1441 self.assertTrue(logs_ts, datetime.strptime(logs_ts, "%Y%m%d%H%M%S")) 1442 self.assertEqual({'0': 'example.org'}, bs_manager.known_hosts) 1443 self.assertIsFalse(bs_manager.has_controller) 1444 1445 def test_from_args_no_host(self): 1446 args = Namespace( 1447 env='foo', juju_bin='bar', debug=True, temp_env_name='baz', 1448 bootstrap_host=None, machine=['example.com'], 1449 series='angsty', agent_url='qux', agent_stream='escaped', 1450 region='eu-west-northwest-5', logs='pine', keep_env=True, 1451 deadline=None, to=None) 1452 with patch('deploy_stack.client_from_config'): 1453 bs_manager = BootstrapManager.from_args(args) 1454 self.assertIs(None, bs_manager.bootstrap_host) 1455 self.assertEqual({}, bs_manager.known_hosts) 1456 1457 def make_client(self): 1458 client = MagicMock() 1459 client.env = JujuData( 1460 'foo', {'type': 'baz'}, use_context(self, temp_dir())) 1461 client.get_matching_agent_version.return_value = '3.14' 1462 client.get_cache_path.return_value = get_cache_path( 1463 client.env.juju_home) 1464 return client 1465 1466 def test_bootstrap_context_calls_update_env(self): 1467 client = fake_juju_client() 1468 client.env.juju_home = use_context(self, temp_dir()) 1469 ue_mock = use_context( 1470 self, patch('deploy_stack.update_env', wraps=update_env)) 1471 wfp_mock = use_context( 1472 self, patch('deploy_stack.wait_for_port', autospec=True)) 1473 bs_manager = BootstrapManager( 1474 'bar', client, client, None, 1475 [], 'wacky', 'url', 'devel', None, client.env.juju_home, False) 1476 bs_manager.known_hosts['0'] = 'bootstrap.example.org' 1477 with bs_manager.bootstrap_context([]): 1478 pass 1479 ue_mock.assert_called_with( 1480 client.env, 'bar', series='wacky', 1481 bootstrap_host='bootstrap.example.org', 1482 agent_url='url', agent_stream='devel', region=None) 1483 wfp_mock.assert_called_once_with( 1484 'bootstrap.example.org', 22, timeout=120) 1485 1486 def test_bootstrap_context_calls_update_env_omit(self): 1487 client = fake_juju_client() 1488 client.env.juju_home = use_context(self, temp_dir()) 1489 ue_mock = use_context( 1490 self, patch('deploy_stack.update_env', wraps=update_env)) 1491 wfp_mock = use_context( 1492 self, patch('deploy_stack.wait_for_port', autospec=True)) 1493 bs_manager = BootstrapManager( 1494 'bar', client, client, None, 1495 [], 'wacky', 'url', 'devel', None, client.env.juju_home, True) 1496 bs_manager.known_hosts['0'] = 'bootstrap.example.org' 1497 with bs_manager.bootstrap_context( 1498 [], omit_config={'bootstrap_host', 'series'}): 1499 pass 1500 ue_mock.assert_called_with(client.env, 'bar', agent_url='url', 1501 agent_stream='devel', region=None) 1502 wfp_mock.assert_called_once_with( 1503 'bootstrap.example.org', 22, timeout=120) 1504 1505 def test_bootstrap_context_sets_has_controller(self): 1506 client = self.make_client() 1507 bs_manager = BootstrapManager( 1508 'foobar', client, client, None, [], None, None, None, None, 1509 None, False) 1510 with patch.object(client, 'kill_controller'): 1511 with bs_manager.bootstrap_context([]): 1512 self.assertIsTrue(bs_manager.has_controller) 1513 self.assertIsTrue(bs_manager.has_controller) 1514 1515 def test_existing_bootstrap_context_sets_has_controller(self): 1516 client = self.make_client() 1517 bs_manager = BootstrapManager( 1518 'foobar', client, client, None, [], None, None, None, None, 1519 None, False) 1520 with patch.object(client, 'kill_controller'): 1521 with bs_manager.existing_bootstrap_context([]): 1522 self.assertIsTrue(bs_manager.has_controller) 1523 self.assertIsTrue(bs_manager.has_controller) 1524 1525 def test_handle_bootstrap_exceptions_ignores_soft_deadline(self): 1526 env = JujuData('foo', {'type': 'nonlocal'}) 1527 client = ModelClient(env, None, None) 1528 tear_down_client = ModelClient(env, None, None) 1529 soft_deadline = datetime(2015, 1, 2, 3, 4, 6) 1530 now = soft_deadline + timedelta(seconds=1) 1531 client.env.juju_home = use_context(self, temp_dir()) 1532 bs_manager = BootstrapManager( 1533 'foobar', client, tear_down_client, None, [], None, None, None, 1534 None, client.env.juju_home, False) 1535 1536 def do_check(*args, **kwargs): 1537 with client.check_timeouts(): 1538 with tear_down_client.check_timeouts(): 1539 return make_fake_juju_return() 1540 1541 with patch.object(bs_manager.tear_down_client, 'juju', 1542 side_effect=do_check, autospec=True): 1543 with patch.object(client._backend, '_now', return_value=now): 1544 fake_exception = Exception() 1545 with self.assertRaises(LoggedException) as exc: 1546 with bs_manager.handle_bootstrap_exceptions(): 1547 client._backend.soft_deadline = soft_deadline 1548 tear_down_client._backend.soft_deadline = soft_deadline 1549 raise fake_exception 1550 self.assertIs(fake_exception, exc.exception.exception) 1551 1552 def test_tear_down_requires_same_env(self): 1553 client = self.make_client() 1554 client.env.juju_home = 'foobar' 1555 tear_down_client = self.make_client() 1556 tear_down_client.env.juju_home = 'barfoo' 1557 bs_manager = BootstrapManager( 1558 'foobar', client, tear_down_client, 1559 None, [], None, None, None, None, client.env.juju_home, False) 1560 with self.assertRaisesRegexp(AssertionError, 1561 'Tear down client needs same env'): 1562 with patch.object(client, 'destroy_controller', 1563 autospec=True) as destroy_mock: 1564 bs_manager.tear_down() 1565 self.assertEqual('barfoo', tear_down_client.env.juju_home) 1566 self.assertIsFalse(destroy_mock.called) 1567 1568 def test_dump_all_multi_model(self): 1569 client = fake_juju_client() 1570 client.bootstrap() 1571 with temp_dir() as log_dir: 1572 bs_manager = BootstrapManager( 1573 'foobar', client, client, 1574 None, [], None, None, None, None, log_dir, False) 1575 with patch('deploy_stack.dump_env_logs_known_hosts') as del_mock: 1576 with patch.object(bs_manager, '_should_dump', 1577 return_value=True): 1578 bs_manager.dump_all_logs() 1579 1580 clients = dict((c[1][0].env.environment, c[1][0]) 1581 for c in del_mock.mock_calls) 1582 self.assertItemsEqual( 1583 [call(client, os.path.join(log_dir, 'name'), None, {}), 1584 call(clients['controller'], os.path.join(log_dir, 'controller'), 1585 'foo/models/cache.yaml', {})], 1586 del_mock.mock_calls) 1587 1588 def test_dump_all_multi_model_iter_failure(self): 1589 client = fake_juju_client() 1590 client.bootstrap() 1591 with temp_dir() as log_dir: 1592 bs_manager = BootstrapManager( 1593 'foobar', client, client, 1594 None, [], None, None, None, None, log_dir, False) 1595 with patch('deploy_stack.dump_env_logs_known_hosts') as del_mock: 1596 with patch.object(client, 'iter_model_clients', 1597 side_effect=Exception): 1598 with patch.object(bs_manager, '_should_dump', 1599 return_value=True): 1600 bs_manager.dump_all_logs() 1601 1602 clients = dict((c[1][0].env.environment, c[1][0]) 1603 for c in del_mock.mock_calls) 1604 1605 self.assertItemsEqual( 1606 [call(client, os.path.join(log_dir, 'name'), None, {}), 1607 call(clients['controller'], os.path.join(log_dir, 'controller'), 1608 'foo/models/cache.yaml', {})], 1609 del_mock.mock_calls) 1610 1611 def test_dump_all_logs_ignores_soft_deadline(self): 1612 1613 def do_check(client, *args, **kwargs): 1614 with client.check_timeouts(): 1615 pass 1616 1617 client = fake_juju_client() 1618 client._backend._past_deadline = True 1619 client.bootstrap() 1620 with temp_dir() as log_dir: 1621 bs_manager = BootstrapManager( 1622 'foobar', client, client, 1623 None, [], None, None, None, None, log_dir, False) 1624 with patch.object(bs_manager, '_should_dump', return_value=True, 1625 autospec=True): 1626 with patch('deploy_stack.dump_env_logs_known_hosts', 1627 side_effect=do_check, autospec=True): 1628 bs_manager.dump_all_logs() 1629 1630 def test_runtime_context_raises_logged_exception(self): 1631 client = fake_juju_client() 1632 client.bootstrap() 1633 bs_manager = BootstrapManager( 1634 'foobar', client, client, 1635 None, [], None, None, None, None, client.env.juju_home, False) 1636 bs_manager.has_controller = True 1637 test_error = Exception("Some exception") 1638 test_error.output = "a stdout value" 1639 test_error.stderr = "a stderr value" 1640 with patch.object(bs_manager, 'dump_all_logs', autospec=True): 1641 with patch('deploy_stack.safe_print_status', 1642 autospec=True) as sp_mock: 1643 with self.assertRaises(LoggedException) as err_ctx: 1644 with bs_manager.runtime_context([]): 1645 raise test_error 1646 self.assertIs(err_ctx.exception.exception, test_error) 1647 self.assertIn("a stdout value", self.log_stream.getvalue()) 1648 self.assertIn("a stderr value", self.log_stream.getvalue()) 1649 sp_mock.assert_called_once_with(client) 1650 1651 def test_runtime_context_raises_logged_exception_no_controller(self): 1652 client = fake_juju_client() 1653 client.bootstrap() 1654 bs_manager = BootstrapManager( 1655 'foobar', client, client, 1656 None, [], None, None, None, None, client.env.juju_home, False) 1657 bs_manager.has_controller = False 1658 test_error = Exception("Some exception") 1659 test_error.output = "a stdout value" 1660 test_error.stderr = "a stderr value" 1661 with patch.object(bs_manager, 'dump_all_logs', autospec=True): 1662 with patch('deploy_stack.safe_print_status', 1663 autospec=True) as sp_mock: 1664 with self.assertRaises(LoggedException) as err_ctx: 1665 with bs_manager.runtime_context([]): 1666 raise test_error 1667 self.assertIs(err_ctx.exception.exception, test_error) 1668 self.assertIn("a stdout value", self.log_stream.getvalue()) 1669 self.assertIn("a stderr value", self.log_stream.getvalue()) 1670 self.assertEqual(0, sp_mock.call_count) 1671 self.assertIn( 1672 "Client lost controller, not calling status", 1673 self.log_stream.getvalue()) 1674 1675 def test_runtime_context_looks_up_host(self): 1676 client = fake_juju_client() 1677 client.bootstrap() 1678 bs_manager = BootstrapManager( 1679 'foobar', client, client, 1680 None, [], None, None, None, None, client.env.juju_home, False) 1681 with patch.object(bs_manager, 'dump_all_logs', autospec=True): 1682 with bs_manager.runtime_context([]): 1683 self.assertEqual({ 1684 '0': '0.example.com'}, bs_manager.known_hosts) 1685 1686 @patch('deploy_stack.dump_env_logs_known_hosts', autospec=True) 1687 def test_runtime_context_addable_machines_no_known_hosts(self, del_mock): 1688 client = fake_juju_client() 1689 client.bootstrap() 1690 bs_manager = BootstrapManager( 1691 'foobar', client, client, 1692 None, [], None, None, None, None, client.env.juju_home, False) 1693 bs_manager.known_hosts = {} 1694 with patch.object(bs_manager.client, 'add_ssh_machines', 1695 autospec=True) as ads_mock: 1696 with patch.object(bs_manager, 'dump_all_logs', autospec=True): 1697 with bs_manager.runtime_context(['baz']): 1698 ads_mock.assert_called_once_with(['baz']) 1699 1700 @patch('deploy_stack.BootstrapManager.dump_all_logs', autospec=True) 1701 def test_runtime_context_addable_machines_with_known_hosts(self, dal_mock): 1702 client = fake_juju_client() 1703 client.bootstrap() 1704 with temp_dir() as log_dir: 1705 bs_manager = BootstrapManager( 1706 'foobar', client, client, 1707 None, [], None, None, None, None, log_dir, False) 1708 bs_manager.known_hosts['0'] = 'example.org' 1709 with patch.object(bs_manager.client, 'add_ssh_machines', 1710 autospec=True) as ads_mock: 1711 with bs_manager.runtime_context(['baz']): 1712 ads_mock.assert_called_once_with(['baz']) 1713 1714 @contextmanager 1715 def no_controller_manager(self): 1716 client = fake_juju_client() 1717 client.bootstrap() 1718 bs_manager = BootstrapManager( 1719 'foobar', client, client, 1720 None, [], None, None, None, None, self.juju_home, False) 1721 bs_manager.has_controller = False 1722 with patch('deploy_stack.safe_print_status', 1723 autospec=True) as sp_mock: 1724 with patch.object( 1725 client, 'juju', wrap=client.juju, 1726 return_value=make_fake_juju_return()) as juju_mock: 1727 with patch.object(client, 'get_juju_output', 1728 wraps=client.get_juju_output) as gjo_mock: 1729 with patch.object(bs_manager, '_should_dump', 1730 return_value=True, autospec=True): 1731 with patch('deploy_stack.get_remote_machines', 1732 return_value={}): 1733 yield bs_manager 1734 self.assertEqual(sp_mock.call_count, 0) 1735 self.assertEqual(0, gjo_mock.call_count) 1736 juju_mock.assert_called_once_with( 1737 'kill-controller', ('name', '-y'), check=True, include_e=False, 1738 timeout=600) 1739 1740 def test_runtime_context_no_controller(self): 1741 with self.no_controller_manager() as bs_manager: 1742 with bs_manager.runtime_context([]): 1743 pass 1744 1745 def test_runtime_context_no_controller_exception(self): 1746 with self.no_controller_manager() as bs_manager: 1747 with self.assertRaises(LoggedException): 1748 with bs_manager.runtime_context([]): 1749 raise ValueError 1750 1751 @contextmanager 1752 def logged_exception_bs_manager(self): 1753 client = fake_juju_client() 1754 with temp_dir() as root: 1755 log_dir = os.path.join(root, 'log-dir') 1756 os.mkdir(log_dir) 1757 bs_manager = BootstrapManager( 1758 'foobar', client, client, 1759 None, [], None, None, None, None, log_dir, False) 1760 juju_home = os.path.join(root, 'juju-home') 1761 os.mkdir(juju_home) 1762 client.env.juju_home = juju_home 1763 yield bs_manager 1764 1765 def test_booted_context_handles_logged_exception(self): 1766 with self.logged_exception_bs_manager() as bs_manager: 1767 with self.assertRaises(SystemExit): 1768 with patch.object(bs_manager, 'dump_all_logs'): 1769 with bs_manager.booted_context(False): 1770 raise LoggedException() 1771 1772 def test_booted_context_raises_logged_exception(self): 1773 with self.logged_exception_bs_manager() as bs_manager: 1774 bs_manager.logged_exception_exit = False 1775 with self.assertRaises(LoggedException): 1776 with patch.object(bs_manager, 'dump_all_logs'): 1777 with bs_manager.booted_context(False): 1778 raise LoggedException() 1779 1780 def test_booted_context_omits_supported(self): 1781 client = fake_juju_client() 1782 client.env.juju_home = use_context(self, temp_dir()) 1783 client.bootstrap_replaces = {'agent-version', 'series', 1784 'bootstrap-host', 'agent-stream'} 1785 ue_mock = use_context( 1786 self, patch('deploy_stack.update_env', wraps=update_env)) 1787 wfp_mock = use_context( 1788 self, patch('deploy_stack.wait_for_port', autospec=True)) 1789 bs_manager = BootstrapManager( 1790 'bar', client, client, 'bootstrap.example.org', 1791 [], 'wacky', 'url', 'devel', None, client.env.juju_home, False) 1792 with patch.object(bs_manager, 'runtime_context'): 1793 with bs_manager.booted_context([]): 1794 pass 1795 self.assertEqual({ 1796 'name': 'bar', 1797 'default-series': 'wacky', 1798 'agent-metadata-url': 'url', 1799 'type': 'foo', 1800 'region': 'bar', 1801 'test-mode': True, 1802 }, client.get_model_config()) 1803 ue_mock.assert_called_with(client.env, 'bar', agent_url='url', 1804 region=None) 1805 wfp_mock.assert_called_once_with( 1806 'bootstrap.example.org', 22, timeout=120) 1807 1808 @contextmanager 1809 def booted_to_bootstrap(self, bs_manager): 1810 """Preform patches to focus on the call to bootstrap.""" 1811 with patch.object(bs_manager, 'dump_all_logs'): 1812 with patch.object(bs_manager, 'runtime_context'): 1813 with patch.object( 1814 bs_manager.client, 'juju', 1815 return_value=make_fake_juju_return()): 1816 with patch.object(bs_manager.client, 'bootstrap') as mock: 1817 yield mock 1818 1819 def test_booted_context_kwargs(self): 1820 client = fake_juju_client() 1821 with temp_dir() as root: 1822 log_dir = os.path.join(root, 'log-dir') 1823 os.mkdir(log_dir) 1824 bs_manager = BootstrapManager( 1825 'foobar', client, client, 1826 None, [], None, None, None, None, log_dir, False) 1827 juju_home = os.path.join(root, 'juju-home') 1828 os.mkdir(juju_home) 1829 client.env.juju_home = juju_home 1830 with self.booted_to_bootstrap(bs_manager) as bootstrap_mock: 1831 with bs_manager.booted_context(False, to='test'): 1832 bootstrap_mock.assert_called_once_with( 1833 upload_tools=False, to='test', bootstrap_series=None) 1834 with self.booted_to_bootstrap(bs_manager) as bootstrap_mock: 1835 with bs_manager.existing_booted_context(False, to='test'): 1836 bootstrap_mock.assert_called_once_with( 1837 upload_tools=False, to='test', bootstrap_series=None) 1838 1839 def test_runtime_context_teardown_ignores_soft_deadline(self): 1840 env = JujuData('foo', {'type': 'nonlocal'}) 1841 soft_deadline = datetime(2015, 1, 2, 3, 4, 6) 1842 now = soft_deadline + timedelta(seconds=1) 1843 client = ModelClient(env, None, None) 1844 tear_down_client = ModelClient(env, None, None) 1845 1846 def do_check_client(*args, **kwargs): 1847 with client.check_timeouts(): 1848 return iter([]) 1849 1850 def do_check_teardown_client(*args, **kwargs): 1851 with tear_down_client.check_timeouts(): 1852 return iter([]) 1853 1854 with temp_dir() as log_dir: 1855 bs_manager = BootstrapManager( 1856 'foobar', client, tear_down_client, 1857 None, [], None, None, None, None, log_dir, False) 1858 bs_manager.known_hosts['0'] = 'example.org' 1859 with patch.object(bs_manager.client, 'juju', 1860 side_effect=do_check_client, autospec=True): 1861 with patch.object(bs_manager.client, 'iter_model_clients', 1862 side_effect=do_check_client, autospec=True, 1863 ): 1864 with patch.object(bs_manager, 'tear_down', 1865 do_check_teardown_client): 1866 with patch.object(client._backend, '_now', 1867 return_value=now): 1868 with bs_manager.runtime_context(['baz']): 1869 client._backend.soft_deadline = soft_deadline 1870 td_backend = tear_down_client._backend 1871 td_backend.soft_deadline = soft_deadline 1872 1873 @contextmanager 1874 def make_bootstrap_manager(self): 1875 client = fake_juju_client() 1876 with temp_dir() as log_dir: 1877 bs_manager = BootstrapManager( 1878 'foobar', client, client, 1879 None, [], None, None, None, None, log_dir, False) 1880 yield bs_manager 1881 1882 def test_top_context_dumps_timings(self): 1883 with self.make_bootstrap_manager() as bs_manager: 1884 with patch('deploy_stack.dump_juju_timings') as djt_mock: 1885 with bs_manager.top_context(): 1886 pass 1887 djt_mock.assert_called_once_with(bs_manager.client, bs_manager.log_dir) 1888 1889 def test_top_context_dumps_timings_on_exception(self): 1890 with self.make_bootstrap_manager() as bs_manager: 1891 with patch('deploy_stack.dump_juju_timings') as djt_mock: 1892 with self.assertRaises(ValueError): 1893 with bs_manager.top_context(): 1894 raise ValueError 1895 djt_mock.assert_called_once_with(bs_manager.client, bs_manager.log_dir) 1896 1897 def test_top_context_no_log_dir_skips_timings(self): 1898 with self.make_bootstrap_manager() as bs_manager: 1899 bs_manager.log_dir = None 1900 with patch('deploy_stack.dump_juju_timings') as djt_mock: 1901 with bs_manager.top_context(): 1902 pass 1903 self.assertEqual(djt_mock.call_count, 0) 1904 1905 def test_collect_resource_details_collects_expected_details(self): 1906 controller_uuid = 'eb67e1eb-6c54-45f5-8b6a-b6243be97202' 1907 members = [ 1908 Machine('0', {'dns-name': '10.0.0.0', 1909 'instance-id': 'juju-aaaa-machine-0'}), 1910 Machine('1', {'dns-name': '10.0.0.1', 1911 'instance-id': 'juju-dddd-machine-1'}), 1912 ] 1913 client = fake_juju_client() 1914 bs_manager = BootstrapManager( 1915 'foobar', client, client, None, [], None, None, None, None, 1916 client.env.juju_home, False) 1917 result = { 1918 'controller-uuid': controller_uuid, 1919 'instances': [(m.info['instance-id'], m.info['dns-name']) 1920 for m in members] 1921 } 1922 1923 with patch.object(client, 'get_controller_uuid', autospec=True, 1924 return_value=controller_uuid): 1925 with patch.object(client, 'get_controller_members', autospec=True, 1926 return_value=members): 1927 bs_manager.collect_resource_details() 1928 self.assertEqual(bs_manager.resource_details, result) 1929 1930 def test_ensure_cleanup(self): 1931 client = fake_juju_client() 1932 with temp_dir() as root: 1933 log_dir = os.path.join(root, 'log-dir') 1934 os.mkdir(log_dir) 1935 bs_manager = BootstrapManager( 1936 'controller', client, client, 1937 None, [], None, None, None, None, log_dir, False) 1938 mock_substrate = Mock() 1939 mock_details = {} 1940 with patch('deploy_stack.make_substrate_manager', autospec=True)\ 1941 as msm: 1942 msm.return_value.__enter__.return_value = mock_substrate 1943 bs_manager.resource_details = mock_details 1944 bs_manager.ensure_cleanup() 1945 mock_substrate.ensure_cleanup.assert_called_once_with( 1946 mock_details) 1947 msm.assert_called_once_with(client.env) 1948 1949 def test_ensure_cleanup_resource_details_empty(self): 1950 client = fake_juju_client() 1951 with temp_dir() as root: 1952 log_dir = os.path.join(root, 'log-dir') 1953 os.mkdir(log_dir) 1954 bs_manager = BootstrapManager( 1955 'controller', client, client, 1956 None, [], None, None, None, None, log_dir, False) 1957 with patch('deploy_stack.make_substrate_manager', autospec=True) \ 1958 as msm: 1959 rl = bs_manager.ensure_cleanup() 1960 self.assertEquals(0, msm.call_count) 1961 self.assertEquals(rl, []) 1962 1963 def test_ensure_cleanup_substrate_none(self): 1964 client = fake_juju_client() 1965 with temp_dir() as root: 1966 log_dir = os.path.join(root, 'log-dir') 1967 os.mkdir(log_dir) 1968 bs_manager = BootstrapManager( 1969 'controller', client, client, 1970 None, [], None, None, None, None, log_dir, False) 1971 mock_details = {} 1972 bs_manager.resource_details = mock_details 1973 with patch('deploy_stack.make_substrate_manager', autospec=True)\ 1974 as msm: 1975 msm.return_value.__enter__.return_value = None 1976 rl = bs_manager.ensure_cleanup() 1977 self.assertIn("foo is an unknown provider." 1978 " Unable to ensure cleanup", 1979 self.log_stream.getvalue()) 1980 self.assertEquals(rl, []) 1981 1982 1983 class TestBootContext(FakeHomeTestCase): 1984 1985 def setUp(self): 1986 super(TestBootContext, self).setUp() 1987 self.addContext(patch('sys.stdout')) 1988 1989 @contextmanager 1990 def bc_context(self, client, log_dir=None, keep_env=False): 1991 dal_mock = self.addContext( 1992 patch('deploy_stack.BootstrapManager.dump_all_logs')) 1993 self.addContext(patch('deploy_stack.get_machine_dns_name', 1994 return_value='foo', autospec=True)) 1995 self.addContext(patch( 1996 'deploy_stack.BootstrapManager.collect_resource_details', 1997 autospec=True)) 1998 models = [{'name': 'controller'}, {'name': 'bar'}] 1999 self.addContext(patch.object(client, '_get_models', 2000 return_value=models, autospec=True)) 2001 po_count = 0 2002 with patch('subprocess.Popen', autospec=True, 2003 return_value=FakePopen( 2004 'kill-controller', '', 0)) as po_mock: 2005 with patch('deploy_stack.BootstrapManager.tear_down', 2006 autospec=True) as tear_down_mock: 2007 with patch.object(client, 'kill_controller', 2008 autospec=True) as kill_mock: 2009 yield 2010 self.assertEqual(po_count, po_mock.call_count) 2011 dal_mock.assert_called_once_with() 2012 tear_down_count = 0 if keep_env else 1 2013 self.assertEqual(1, kill_mock.call_count) 2014 self.assertEqual(tear_down_count, tear_down_mock.call_count) 2015 2016 def test_bootstrap_context(self): 2017 cc_mock = self.addContext(patch('subprocess.check_call')) 2018 client = ModelClient(JujuData( 2019 'foo', {'type': 'paas', 'region': 'qux'}), '1.23', 'path') 2020 with self.bc_context(client, 'log_dir'): 2021 with observable_temp_file() as config_file: 2022 with boot_context('bar', client, None, [], None, None, None, 2023 'log_dir', keep_env=False, 2024 upload_tools=False): 2025 pass 2026 assert_juju_call(self, cc_mock, client, ( 2027 'path', '--show-log', 'bootstrap', '--constraints', 2028 'mem=2G', 'paas/qux', 'bar', '--config', config_file.name, 2029 '--add-model', 'bar', '--agent-version', '1.23'), 0) 2030 assert_juju_call(self, cc_mock, client, ( 2031 'path', '--show-log', 'list-controllers'), 1) 2032 assert_juju_call(self, cc_mock, client, ( 2033 'path', '--show-log', 'list-models', '-c', 'bar'), 2) 2034 assert_juju_call(self, cc_mock, client, ( 2035 'path', '--show-log', 'status', '-m', 'bar:controller', 2036 '--format', 'yaml'), 3) 2037 assert_juju_call(self, cc_mock, client, ( 2038 'path', '--show-log', 'status', '-m', 'bar:bar', 2039 '--format', 'yaml'), 4) 2040 2041 def test_keep_env(self): 2042 cc_mock = self.addContext(patch('subprocess.check_call')) 2043 client = ModelClient(JujuData( 2044 'foo', {'type': 'paas', 'region': 'qux'}), '1.23', 'path') 2045 with self.bc_context(client, keep_env=True): 2046 with observable_temp_file() as config_file: 2047 with boot_context('bar', client, None, [], None, None, None, 2048 None, keep_env=True, upload_tools=False): 2049 pass 2050 assert_juju_call(self, cc_mock, client, ( 2051 'path', '--show-log', 'bootstrap', '--constraints', 2052 'mem=2G', 'paas/qux', 'bar', '--config', config_file.name, 2053 '--add-model', 'bar', '--agent-version', '1.23'), 0) 2054 assert_juju_call(self, cc_mock, client, ( 2055 'path', '--show-log', 'list-controllers'), 1) 2056 assert_juju_call(self, cc_mock, client, ( 2057 'path', '--show-log', 'list-models', '-c', 'bar'), 2) 2058 assert_juju_call(self, cc_mock, client, ( 2059 'path', '--show-log', 'status', '-m', 'bar:controller', 2060 '--format', 'yaml'), 3) 2061 assert_juju_call(self, cc_mock, client, ( 2062 'path', '--show-log', 'status', '-m', 'bar:bar', 2063 '--format', 'yaml'), 4) 2064 2065 def test_upload_tools(self): 2066 cc_mock = self.addContext(patch('subprocess.check_call')) 2067 client = ModelClient(JujuData( 2068 'foo', {'type': 'paas', 'region': 'qux'}), '1.23', 'path') 2069 with self.bc_context(client): 2070 with observable_temp_file() as config_file: 2071 with boot_context('bar', client, None, [], None, None, None, 2072 None, keep_env=False, upload_tools=True): 2073 pass 2074 assert_juju_call(self, cc_mock, client, ( 2075 'path', '--show-log', 'bootstrap', '--upload-tools', 2076 '--constraints', 'mem=2G', 'paas/qux', 'bar', '--config', 2077 config_file.name, '--add-model', 'bar'), 0) 2078 2079 def test_calls_update_env(self): 2080 cc_mock = self.addContext(patch('subprocess.check_call')) 2081 client = ModelClient(JujuData( 2082 'foo', {'type': 'paas', 'region': 'qux'}), '2.3', 'path') 2083 ue_mock = self.addContext( 2084 patch('deploy_stack.update_env', wraps=update_env)) 2085 with self.bc_context(client): 2086 with observable_temp_file() as config_file: 2087 with boot_context('bar', client, None, [], 'wacky', 'url', 2088 'devel', None, keep_env=False, 2089 upload_tools=False): 2090 pass 2091 ue_mock.assert_called_with( 2092 client.env, 'bar', agent_url='url', agent_stream='devel', 2093 series='wacky', bootstrap_host=None, region=None) 2094 assert_juju_call(self, cc_mock, client, ( 2095 'path', '--show-log', 'bootstrap', '--constraints', 'mem=2G', 2096 'paas/qux', 'bar', '--config', config_file.name, 2097 '--add-model', 'bar', '--agent-version', '2.3', 2098 '--bootstrap-series', 'wacky'), 0) 2099 2100 def test_with_bootstrap_failure(self): 2101 2102 class FakeException(Exception): 2103 """A sentry exception to be raised by bootstrap.""" 2104 2105 client = ModelClient(JujuData( 2106 'foo', {'type': 'paas'}), '1.23', 'path') 2107 self.addContext(patch('deploy_stack.get_machine_dns_name', 2108 return_value='foo')) 2109 self.addContext(patch('subprocess.check_call')) 2110 tear_down_mock = self.addContext( 2111 patch('deploy_stack.BootstrapManager.tear_down', autospec=True)) 2112 kill_mock = self.addContext( 2113 patch('jujupy.ModelClient.kill_controller', autospec=True)) 2114 po_mock = self.addContext(patch( 2115 'subprocess.Popen', autospec=True, 2116 return_value=FakePopen('kill-controller', '', 0))) 2117 self.addContext(patch('deploy_stack.wait_for_port')) 2118 fake_exception = FakeException() 2119 self.addContext(patch.object(client, 'bootstrap', 2120 side_effect=fake_exception)) 2121 crl_mock = self.addContext(patch('deploy_stack.copy_remote_logs')) 2122 al_mock = self.addContext(patch('deploy_stack.archive_logs')) 2123 le_mock = self.addContext(patch('logging.exception')) 2124 with self.assertRaises(SystemExit): 2125 with boot_context('bar', client, 'baz', [], None, None, None, 2126 'log_dir', keep_env=False, upload_tools=True): 2127 pass 2128 le_mock.assert_called_once_with(fake_exception) 2129 self.assertEqual(crl_mock.call_count, 1) 2130 call_args = crl_mock.call_args[0] 2131 self.assertIsInstance(call_args[0], _Remote) 2132 self.assertEqual(call_args[0].get_address(), 'baz') 2133 self.assertEqual(call_args[1], 'log_dir') 2134 al_mock.assert_called_once_with('log_dir') 2135 self.assertEqual(0, tear_down_mock.call_count) 2136 self.assertEqual(2, kill_mock.call_count) 2137 self.assertEqual(0, po_mock.call_count) 2138 2139 def test_region(self): 2140 self.addContext(patch('subprocess.check_call', autospec=True)) 2141 client = ModelClient(JujuData( 2142 'foo', {'type': 'paas'}), '1.23', 'path') 2143 with self.bc_context(client, 'log_dir'): 2144 with boot_context('bar', client, None, [], None, None, None, 2145 'log_dir', keep_env=False, upload_tools=False, 2146 region='steve'): 2147 pass 2148 self.assertEqual('steve', client.env.get_region()) 2149 2150 def test_status_error_raises(self): 2151 """An error on final status propagates so an assess will fail.""" 2152 error = subprocess.CalledProcessError(1, ['juju'], '') 2153 effects = [None, None, None, None, None, None, error] 2154 cc_mock = self.addContext(patch('subprocess.check_call', autospec=True, 2155 side_effect=effects)) 2156 client = ModelClient(JujuData( 2157 'foo', {'type': 'paas', 'region': 'qux'}), '1.23', 'path') 2158 with self.bc_context(client, 'log_dir'): 2159 with observable_temp_file() as config_file: 2160 with self.assertRaises(subprocess.CalledProcessError) as ctx: 2161 with boot_context('bar', client, None, [], None, None, 2162 None, 'log_dir', keep_env=False, 2163 upload_tools=False): 2164 pass 2165 self.assertIs(ctx.exception, error) 2166 assert_juju_call(self, cc_mock, client, ( 2167 'path', '--show-log', 'bootstrap', '--constraints', 2168 'mem=2G', 'paas/qux', 'bar', '--config', config_file.name, 2169 '--add-model', 'bar', '--agent-version', '1.23'), 0) 2170 assert_juju_call(self, cc_mock, client, ( 2171 'path', '--show-log', 'list-controllers'), 1) 2172 assert_juju_call(self, cc_mock, client, ( 2173 'path', '--show-log', 'list-models', '-c', 'bar'), 2) 2174 assert_juju_call(self, cc_mock, client, ( 2175 'path', '--show-log', 'status', '-m', 'bar:controller', 2176 '--format', 'yaml'), 3) 2177 assert_juju_call(self, cc_mock, client, ( 2178 'path', '--show-log', 'status', '-m', 'bar:bar', 2179 '--format', 'yaml'), 4) 2180 2181 2182 class TestDeployJobParseArgs(FakeHomeTestCase): 2183 2184 def test_deploy_job_parse_args(self): 2185 args = deploy_job_parse_args(['foo', 'bar/juju', 'baz', 'qux']) 2186 self.assertEqual(args, Namespace( 2187 agent_stream=None, 2188 agent_url=None, 2189 bootstrap_host=None, 2190 debug=False, 2191 env='foo', 2192 temp_env_name='qux', 2193 keep_env=False, 2194 logs='baz', 2195 machine=[], 2196 juju_bin='bar/juju', 2197 series=None, 2198 upgrade=False, 2199 verbose=logging.INFO, 2200 upload_tools=False, 2201 region=None, 2202 to=None, 2203 deadline=None, 2204 controller_host=None, 2205 use_charmstore=False, 2206 )) 2207 2208 def test_upload_tools(self): 2209 args = deploy_job_parse_args( 2210 ['foo', 'bar/juju', 'baz', 'qux', '--upload-tools']) 2211 self.assertEqual(args.upload_tools, True) 2212 2213 def test_agent_stream(self): 2214 args = deploy_job_parse_args( 2215 ['foo', 'bar/juju', 'baz', 'qux', '--agent-stream', 'wacky']) 2216 self.assertEqual('wacky', args.agent_stream) 2217 2218 def test_use_charmstore(self): 2219 args = deploy_job_parse_args( 2220 ['foo', 'bar/juju', 'baz', 'qux', '--use-charmstore']) 2221 self.assertIs(args.use_charmstore, True) 2222 2223 2224 class TestWaitForStateServerToShutdown(FakeHomeTestCase): 2225 2226 def test_openstack(self): 2227 env = JujuData('foo', { 2228 'type': 'openstack', 2229 'region': 'lcy05', 2230 'username': 'steve', 2231 'password': 'password1', 2232 'tenant-name': 'steven', 2233 'auth-url': 'http://example.org', 2234 }, self.juju_home) 2235 client = fake_juju_client(env=env) 2236 with patch('deploy_stack.wait_for_port', autospec=True) as wfp_mock: 2237 with patch('deploy_stack.has_nova_instance', autospec=True, 2238 return_value=False) as hni_mock: 2239 with patch('deploy_stack.print_now', autospec=True) as pn_mock: 2240 wait_for_state_server_to_shutdown( 2241 'example.org', client, 'i-255') 2242 self.assertEqual(pn_mock.mock_calls, [ 2243 call('Waiting for port to close on example.org'), 2244 call('Closed.'), 2245 call('i-255 was removed from nova list'), 2246 ]) 2247 wfp_mock.assert_called_once_with('example.org', 17070, closed=True, 2248 timeout=60) 2249 hni_mock.assert_called_once_with(client.env, 'i-255') 2250 2251 2252 class TestErrorIfUnclean(FakeHomeTestCase): 2253 def test_empty_unclean_resources(self): 2254 uncleaned_resources = [] 2255 error_if_unclean(uncleaned_resources) 2256 self.assertEquals(self.log_stream.getvalue(), '') 2257 2258 def test_contain_unclean_resources(self): 2259 uncleaned_resources = [ 2260 { 2261 'resource': 'instances', 2262 'errors': [('ifoo', 'err-msg'), ('ibar', 'err-msg')] 2263 }, 2264 { 2265 'resource': 'security groups', 2266 'errors': [('sg-bar', 'err-msg')] 2267 } 2268 ] 2269 error_if_unclean(uncleaned_resources) 2270 self.assertListEqual(self.log_stream.getvalue().splitlines(), [ 2271 "CRITICAL Following resource requires manual cleanup", 2272 "CRITICAL instances", 2273 "CRITICAL \tifoo: err-msg", 2274 "CRITICAL \tibar: err-msg", 2275 "CRITICAL security groups", 2276 "CRITICAL \tsg-bar: err-msg" 2277 ]) 2278 2279 def test_unclean_resources_without_sg_error(self): 2280 uncleaned_resources = [ 2281 { 2282 'resource': 'instances', 2283 'errors': [('ifoo', 'err-msg'), ('ibar', 'err-msg')] 2284 }, 2285 ] 2286 error_if_unclean(uncleaned_resources) 2287 self.assertListEqual(self.log_stream.getvalue().splitlines(), [ 2288 "CRITICAL Following resource requires manual cleanup", 2289 "CRITICAL instances", 2290 "CRITICAL \tifoo: err-msg", 2291 "CRITICAL \tibar: err-msg", 2292 ]) 2293 2294 def test_unclean_resources_without_instances_error(self): 2295 uncleaned_resources = [ 2296 { 2297 'resource': 'security groups', 2298 'errors': [('sg-bar', 'err-msg')] 2299 } 2300 ] 2301 error_if_unclean(uncleaned_resources) 2302 self.assertListEqual(self.log_stream.getvalue().splitlines(), [ 2303 "CRITICAL Following resource requires manual cleanup", 2304 "CRITICAL security groups", 2305 "CRITICAL \tsg-bar: err-msg" 2306 ])