github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/acceptancetests/assess_heterogeneous_control.py (about)

     1  #!/usr/bin/env python3
     2  
     3  from argparse import ArgumentParser
     4  from contextlib import contextmanager
     5  import logging
     6  from textwrap import dedent
     7  from subprocess import CalledProcessError
     8  import sys
     9  
    10  from utility import (
    11      configure_logging,
    12      until_timeout,
    13  )
    14  from jujuci import add_credential_args
    15  from jujucharm import (
    16      local_charm_path,
    17  )
    18  from deploy_stack import (
    19      BootstrapManager,
    20      check_token,
    21      get_random_string,
    22      )
    23  
    24  from jujupy import (
    25      client_from_config,
    26      fake_juju_client,
    27      JujuData,
    28      )
    29  
    30  
    31  def prepare_dummy_env(client):
    32      """Use a client to prepare a dummy environment."""
    33      charm_source = local_charm_path(
    34          charm='dummy-source', juju_ver=client.version)
    35      client.deploy(charm_source)
    36      charm_sink = local_charm_path(charm='dummy-sink', juju_ver=client.version)
    37      client.deploy(charm_sink)
    38      token = get_random_string()
    39      client.set_config('dummy-source', {'token': token})
    40      client.juju('integrate', ('dummy-source', 'dummy-sink'))
    41      client.juju('expose', ('dummy-sink',))
    42      return token
    43  
    44  
    45  def get_clients(initial, other, base_env, debug, agent_url):
    46      """Return the clients to use for testing."""
    47      if initial == 'FAKE':
    48          environment = JujuData.from_config(base_env)
    49          client = fake_juju_client(env=environment)
    50          return client, client, client
    51  
    52      initial_client = client_from_config(base_env, initial, debug=debug)
    53      environment = initial_client.env
    54      if agent_url is None:
    55          environment.discard_option('tools-metadata-url')
    56      other_client = initial_client.clone_from_path(other)
    57      # This used to catch an exception of the config didn't match.
    58      # version_client no longer exists so that no longer made sense.
    59      released_client = initial_client.clone_from_path(None)
    60      # If system juju is used, ensure it has identical env to
    61      # initial_client.
    62      released_client.env = initial_client.env
    63      return initial_client, other_client, released_client
    64  
    65  
    66  def assess_heterogeneous(initial, other, base_env, environment_name, log_dir,
    67                           upload_tools, debug, agent_url, agent_stream, series):
    68      """Top level function that prepares the clients and environment.
    69  
    70      initial and other are paths to the binary used initially, and a binary
    71      used later.  base_env is the name of the environment to base the
    72      environment on and environment_name is the new name for the environment.
    73      """
    74      initial_client, other_client, teardown_client = get_clients(
    75          initial, other, base_env, debug, agent_url)
    76      bs_manager = BootstrapManager(
    77          environment_name, initial_client, teardown_client,
    78          bootstrap_host=None, machines=[], series=series, arch=None,
    79          agent_url=agent_url, agent_stream=agent_stream, region=None,
    80          log_dir=log_dir, keep_env=False)
    81      test_control_heterogeneous(bs_manager, other_client, upload_tools)
    82  
    83  
    84  @contextmanager
    85  def run_context(bs_manager, other, upload_tools):
    86      try:
    87          bs_manager.keep_env = True
    88          with bs_manager.booted_context(upload_tools):
    89              if other.env.juju_home != bs_manager.client.env.juju_home:
    90                  raise AssertionError('Juju home out of sync')
    91              yield
    92          # Test clean shutdown of an environment.
    93          callback_with_fallback(other, bs_manager.tear_down_client,
    94                                 nice_tear_down)
    95      except Exception:
    96          bs_manager.tear_down()
    97          raise
    98  
    99  
   100  def test_control_heterogeneous(bs_manager, other, upload_tools):
   101      """Test if one binary can control an environment set up by the other."""
   102      initial = bs_manager.client
   103      released = bs_manager.tear_down_client
   104      with run_context(bs_manager, other, upload_tools):
   105          token = prepare_dummy_env(initial)
   106          initial.wait_for_started()
   107          if sys.platform != "win32":
   108              # Currently, juju ssh is not working on Windows.
   109              check_token(initial, token)
   110              check_series(other)
   111              other.juju('exec', ('--all', 'uname -a'))
   112          other.get_config('dummy-source')
   113          other.get_model_config()
   114          other.juju('remove-relation', ('dummy-source', 'dummy-sink'))
   115          status = other.get_status()
   116          other.juju('unexpose', ('dummy-sink',))
   117          status = other.get_status()
   118          if status.get_applications()['dummy-sink']['exposed']:
   119              raise AssertionError('dummy-sink is still exposed')
   120          status = other.get_status()
   121          charm_path = local_charm_path(
   122              charm='dummy-sink', juju_ver=other.version)
   123          juju_with_fallback(other, released, 'deploy',
   124                             (charm_path, 'sink2'))
   125          other.wait_for_started()
   126          other.juju('integrate', ('dummy-source', 'sink2'))
   127          status = other.get_status()
   128          other.juju('expose', ('sink2',))
   129          status = other.get_status()
   130          if 'sink2' not in status.get_applications():
   131              raise AssertionError('Sink2 missing')
   132          other.remove_application('sink2')
   133          for ignored in until_timeout(30):
   134              status = other.get_status()
   135              if 'sink2' not in status.get_applications():
   136                  break
   137          else:
   138              raise AssertionError('Sink2 not destroyed')
   139          other.juju('integrate', ('dummy-source', 'dummy-sink'))
   140          status = other.get_status()
   141          relations = status.get_applications()['dummy-sink']['relations']
   142          if not relations['source'] == ['dummy-source']:
   143              raise AssertionError('source is not dummy-source.')
   144          other.juju('expose', ('dummy-sink',))
   145          status = other.get_status()
   146          if not status.get_applications()['dummy-sink']['exposed']:
   147              raise AssertionError('dummy-sink is not exposed')
   148          other.juju('add-unit', ('dummy-sink',))
   149          if not has_agent(other, 'dummy-sink/1'):
   150              raise AssertionError('dummy-sink/1 was not added.')
   151          other.juju('remove-unit', ('dummy-sink/1',))
   152          status = other.get_status()
   153          if has_agent(other, 'dummy-sink/1'):
   154              raise AssertionError('dummy-sink/1 was not removed.')
   155          container_type = other.preferred_container()
   156          other.juju('add-machine', (container_type,))
   157          status = other.get_status()
   158          container_machine, = set(k for k, v in status.agent_items() if
   159                                   k.endswith('/{}/0'.format(container_type)))
   160          container_holder = container_machine.split('/')[0]
   161          other.remove_machine(container_machine)
   162          wait_until_removed(other, container_machine)
   163          other.remove_machine(container_holder)
   164          wait_until_removed(other, container_holder)
   165  
   166  
   167  # suppress nosetests
   168  test_control_heterogeneous.__test__ = False
   169  
   170  
   171  def juju_with_fallback(other, released, command, args, include_e=True):
   172      """Fallback to released juju when 1.18 fails.
   173  
   174      Get as much test coverage of 1.18 as we can, by falling back to a released
   175      juju for commands that we expect to fail (due to unsupported agent version
   176      format).
   177      """
   178      def call_juju(client):
   179          client.juju(command, args, include_e=include_e)
   180      return callback_with_fallback(other, released, call_juju)
   181  
   182  
   183  def callback_with_fallback(other, released, callback):
   184      for client in [other, released]:
   185          try:
   186              callback(client)
   187          except CalledProcessError:
   188              if not client.version.startswith('1.18.'):
   189                  raise
   190          else:
   191              break
   192  
   193  
   194  def nice_tear_down(client):
   195      client.kill_controller()
   196  
   197  
   198  def has_agent(client, agent_id):
   199      return bool(agent_id in dict(client.get_status().agent_items()))
   200  
   201  
   202  def wait_until_removed(client, agent_id):
   203      """Wait for an agent to be removed from the environment."""
   204      for ignored in until_timeout(240):
   205          if not has_agent(client, agent_id):
   206              return
   207  
   208      raise AssertionError('Machine not destroyed: {}.'.format(agent_id))
   209  
   210  
   211  def check_series(client, machine='0', series=None):
   212      """Use 'juju ssh' to check that the deployed series meets expectations."""
   213      result = client.get_juju_output('ssh', machine, 'lsb_release', '-c')
   214      label, codename = result.rstrip().split('\t')
   215      if label != 'Codename:':
   216          raise AssertionError()
   217      if series:
   218          expected_codename = series
   219      else:
   220          expected_codename = client.env.get_option('default-series')
   221      if codename != expected_codename:
   222          raise AssertionError(
   223              'Series is {}, not {}'.format(codename, expected_codename))
   224  
   225  
   226  def parse_args(argv=None):
   227      parser = ArgumentParser(description=dedent("""\
   228          Determine whether one juju version can control an environment created
   229          by another version.
   230      """))
   231      parser.add_argument('initial', help='The initial juju binary.')
   232      parser.add_argument('other', help='A different juju binary.')
   233      parser.add_argument('base_environment', help='The environment to base on.')
   234      parser.add_argument('environment_name', help='The new environment name.')
   235      parser.add_argument('log_dir', help='The directory to dump logs to.')
   236      parser.add_argument(
   237          '--upload-tools', action='store_true', default=False,
   238          help='Upload local version of tools before bootstrapping.')
   239      parser.add_argument('--debug', help='Run juju with --debug',
   240                          action='store_true', default=False)
   241      parser.add_argument('--agent-url', default=None)
   242      parser.add_argument('--agent-stream', action='store',
   243                          help='URL for retrieving agent binaries.')
   244      parser.add_argument('--series', action='store',
   245                          help='Name of the Ubuntu series to use.')
   246      add_credential_args(parser)
   247      return parser.parse_args(argv)
   248  
   249  
   250  def main():
   251      args = parse_args()
   252      configure_logging(logging.INFO)
   253      assess_heterogeneous(args.initial, args.other, args.base_environment,
   254                           args.environment_name, args.log_dir,
   255                           args.upload_tools, args.debug, args.agent_url,
   256                           args.agent_stream, args.series)
   257  
   258  
   259  if __name__ == '__main__':
   260      main()