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          ])