github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_agent_metadata.py (about)

     1  #!/usr/bin/env python
     2  """
     3   Juju agent-metadata-url validation on passing as an argument during
     4   juju bootstrap
     5  
     6   Juju agent-metadata-url validation using cloud definition yaml file to
     7   verify that agent-metadata-url specified in the yaml file is applied
     8   while running juju boostrap command.
     9  
    10   Usage: python assess_agent_metadata.py --agent-file=/path/to/juju-*.tgz
    11  
    12   Example: python assess_agent_metadata.py
    13                  --agent-file=/home/juju/juju-2.0.1-xenial-amd64.tgz
    14  """
    15  
    16  from __future__ import print_function
    17  
    18  from argparse import ArgumentParser
    19  from contextlib import contextmanager
    20  from hashlib import sha256
    21  
    22  import logging
    23  import os
    24  import subprocess
    25  import sys
    26  import json
    27  
    28  from deploy_stack import (
    29      BootstrapManager,
    30      )
    31  
    32  from utility import (
    33      add_basic_testing_arguments,
    34      configure_logging,
    35      JujuAssertionError,
    36      temp_dir,
    37      temp_yaml_file,
    38  )
    39  from remote import (
    40      remote_from_unit,
    41      remote_from_address,
    42  )
    43  from jujucharm import (
    44      local_charm_path,
    45  )
    46  
    47  log = logging.getLogger("assess_agent_metadata")
    48  
    49  
    50  def get_sha256_sum(filename):
    51      """
    52      Get SHA256 sum of the given filename
    53      :param filename: A string representing the filename to operate on
    54      """
    55      with open(filename, 'rb') as infile:
    56          return sha256(infile.read()).hexdigest()
    57  
    58  
    59  def assert_cloud_details_are_correct(client, cloud_name, example_cloud):
    60      """
    61      Check juju add-cloud is performed successfully and it is available
    62      in the juju client.
    63      :param client: The juju client
    64      :param cloud_name: The name of the cloud added
    65      :param example_cloud: The content of the cloud
    66      """
    67      clouds = client.env.read_clouds()
    68      try:
    69          if clouds['clouds'][cloud_name] != example_cloud:
    70              raise JujuAssertionError('Cloud mismatch')
    71      except KeyError:
    72          raise JujuAssertionError('Cloud missing {}'.format(cloud_name))
    73  
    74  
    75  def get_local_url_and_sha256(agent_dir, controller_url, agent_stream):
    76      """
    77      Get the agent URL (local file location: file:///) and SHA256
    78      of the agent-file passed
    79      :param agent_dir: The top level directory location of agent file.
    80      :param controller_url: The controller used agent file url
    81      :param agent_stream: String representing agent stream name
    82      """
    83      local_url = os.path.join(agent_dir, "tools", agent_stream,
    84                               os.path.basename(controller_url))
    85  
    86      local_sha256 = get_sha256_sum(local_url)
    87      local_file_path = "file://" + local_url
    88      return local_file_path, local_sha256
    89  
    90  
    91  def get_controller_url_and_sha256(client):
    92      """
    93      Get the agent url and sha256 of the launched client
    94      :param client: Juju client
    95      """
    96      controller_client = client.get_controller_client()
    97      output = controller_client.run(
    98          ['cat /var/lib/juju/tools/machine-0/downloaded-tools.txt'],
    99          machines=['0'])
   100      stdout_details = json.loads(output[0]['Stdout'])
   101      return stdout_details['url'], stdout_details['sha256']
   102  
   103  
   104  def assert_metadata_is_correct(expected_agent_metadata_url, client):
   105      """
   106      verify the client agent-metadata-url matches the specified value
   107      :param expected_agent_metadata_url: The expected agent file path.
   108      :param client: Juju client
   109      """
   110      data = client.get_model_config()
   111      actual_agent_metadata_url = data['agent-metadata-url']['value']
   112      if expected_agent_metadata_url != actual_agent_metadata_url:
   113          raise JujuAssertionError(
   114              'agent-metadata-url mismatch. Expected: {} Got: {}'.format(
   115                  expected_agent_metadata_url, actual_agent_metadata_url))
   116  
   117      log.info('bootstrap successfully with agent-metadata-url={}'.format(
   118          actual_agent_metadata_url))
   119  
   120  
   121  def verify_deployed_charm(client, remote, unit="0"):
   122      """Verify the deployed charm
   123  
   124      Make sure deployed charm uses the same juju tool of the
   125      controller by verifying the sha256 sum
   126  
   127      :param client: Juju client
   128      :param remote: The remote object of the deployed charm
   129      :param unit: String representation of deployed charm unit.
   130      """
   131      output = remote.cat(
   132          "/var/lib/juju/tools/machine-{}/downloaded-tools.txt".format(unit))
   133  
   134      deserialized_output = json.loads(output)
   135      _, controller_sha256 = get_controller_url_and_sha256(client)
   136  
   137      if deserialized_output['sha256'] != controller_sha256:
   138          raise JujuAssertionError(
   139              'agent-metadata-url mismatch. Expected: {} Got: {}'.format(
   140                  controller_sha256, deserialized_output))
   141  
   142      log.info("Charm verification done successfully")
   143  
   144  
   145  def deploy_machine_and_verify(client, series="bionic"):
   146      """Deploy machine using juju add-machine of specified series
   147      and verify that it make use of same agent-file used by the
   148      controller.
   149  
   150      :param client: Juju client
   151      :param series: The charm series to deploy
   152      """
   153      old_status = client.get_status()
   154      client.juju('add-machine', ('--series', series))
   155      new_status = client.wait_for_started()
   156  
   157      # This will iterate only once because we just added only one
   158      # machine after old_status to new_status.
   159      for unit, machine in new_status.iter_new_machines(old_status):
   160          hostname = machine.get('dns-name')
   161          if hostname:
   162              remote = remote_from_address(hostname, machine.get('series'))
   163              verify_deployed_charm(client, remote, unit)
   164          else:
   165              raise JujuAssertionError(
   166                  'Unable to get information about added machine')
   167  
   168      log.info("add-machine verification done successfully")
   169  
   170  
   171  def deploy_charm_and_verify(client, series="bionic", charm_app="dummy-source"):
   172      """
   173      Deploy dummy charm from local repository and
   174      verify it uses the specified agent-metadata-url option
   175      :param client: Juju client
   176      :param series: The charm series to deploy
   177      :param charm_app: Juju charm application
   178      """
   179      charm_source = local_charm_path(
   180          charm=charm_app, juju_ver=client.version, series=series)
   181      client.deploy(charm_source)
   182      client.wait_for_started()
   183      client.set_config(charm_app, {'token': 'one'})
   184      client.wait_for_workloads()
   185      remote = remote_from_unit(client, "{}/0".format(charm_app))
   186      verify_deployed_charm(client, remote)
   187      log.info(
   188          "Successfully deployed charm {} of series {} and verified".format(
   189              "dummy-source", series))
   190  
   191  
   192  def verify_deployed_tool(agent_dir, client, agent_stream):
   193      """
   194      Verify the bootstrapped controller makes use of the the specified
   195      agent-metadata-url.
   196      :param agent_dir:  The top level directory location of agent file.
   197      :param client: Juju client
   198      :param agent_stream: String representing agent stream name
   199      """
   200      controller_url, controller_sha256 = get_controller_url_and_sha256(client)
   201  
   202      local_url, local_sha256 = get_local_url_and_sha256(
   203          agent_dir, controller_url, agent_stream)
   204  
   205      if local_url != controller_url:
   206          raise JujuAssertionError(
   207              "mismatch local URL {} and controller URL {}".format(
   208                  local_url, controller_url))
   209  
   210      if local_sha256 != controller_sha256:
   211          raise JujuAssertionError(
   212              "mismatch local SHA256 {} and controller SHA256 {}".format(
   213                  local_sha256, controller_sha256))
   214  
   215  
   216  def set_new_log_dir(bs_manager, dir_name):
   217      log_dir = os.path.join(bs_manager.log_dir, dir_name)
   218      os.mkdir(log_dir)
   219      bs_manager.log_dir = log_dir
   220  
   221  
   222  def get_controller_series_and_alternative_series(client):
   223      """Get controller and alternative controller series
   224  
   225      Returns the series used by the controller and an alternative series
   226      that is not used by the controller.
   227  
   228      :param client: The juju client
   229      :return: controller and non-controller series
   230      """
   231      supported_series = ['bionic', 'xenial', 'trusty', 'zesty']
   232      controller_status = client.get_status(controller=True)
   233      machines = dict(controller_status.iter_machines())
   234      controller_series = machines['0']['series']
   235      try:
   236          supported_series.remove(controller_series)
   237      except:
   238          raise ValueError("Unknown series {}".format(controller_series))
   239      return controller_series, supported_series[0]
   240  
   241  
   242  def assess_metadata(args, agent_dir, agent_stream):
   243      """
   244      Bootstrap juju controller with agent-metadata-url value
   245      and verify that bootstrapped controller makes use of specified
   246      agent-metadata-url value.
   247      :param args: Parsed command line arguments
   248      :param agent_dir: The top level directory location of agent file.
   249      :param agent_stream: String representing agent stream name
   250      """
   251      bs_manager = BootstrapManager.from_args(args)
   252      client = bs_manager.client
   253      agent_metadata_url = os.path.join(agent_dir, "tools")
   254  
   255      client.env.discard_option('tools-metadata-url')
   256      client.env.update_config(
   257          {
   258              'agent-metadata-url': agent_metadata_url,
   259              'agent-stream': agent_stream
   260          }
   261      )
   262      log.info('bootstrap to use --agent_metadata_url={}'.format(
   263          agent_metadata_url))
   264      client.generate_tool(agent_dir, agent_stream)
   265      set_new_log_dir(bs_manager, "assess_metadata")
   266  
   267      with bs_manager.booted_context(args.upload_tools):
   268          log.info('Metadata bootstrap successful.')
   269          assert_metadata_is_correct(agent_metadata_url, client)
   270          verify_deployed_tool(agent_dir, client, agent_stream)
   271          log.info("Successfully deployed and verified agent-metadata-url")
   272          series_details = get_controller_series_and_alternative_series(client)
   273          controller_series, alt_controller_series = series_details
   274          deploy_charm_and_verify(client, controller_series, "dummy-source")
   275          deploy_machine_and_verify(client, alt_controller_series)
   276  
   277  
   278  def get_cloud_details(client, agent_metadata_url, agent_stream):
   279      """
   280      Create a cloud detail content to be used during add-cloud.
   281      :param client: Juju client
   282      :param agent_metadata_url: Sting representing agent-metadata-url
   283      :param agent_stream: String representing agent stream name
   284      """
   285      cloud_name = client.env.get_cloud()
   286      cloud_details = {
   287          'clouds': {
   288              cloud_name: {
   289                  'type': client.env.provider,
   290                  'regions': {client.env.get_region(): {}},
   291                  'config': {
   292                      'agent-metadata-url': 'file://{}'.format(
   293                          agent_metadata_url),
   294                      'agent-stream': agent_stream,
   295                  }
   296              }
   297          }
   298      }
   299      return cloud_details
   300  
   301  
   302  def assess_add_cloud(args, agent_dir, agent_stream):
   303      """
   304      Perform juju add-cloud by creating a yaml file for cloud
   305      with agent-metadata-url value and bootstrap the juju environment.
   306      :param args: Parsed command line arguments
   307      :param agent_dir: The top level directory location of agent file.
   308      :param agent_stream: String representing agent stream name
   309      """
   310  
   311      bs_manager = BootstrapManager.from_args(args)
   312      client = bs_manager.client
   313      agent_metadata_url = os.path.join(agent_dir, "tools")
   314      # Remove the tool metadata url from the config (note the name, is
   315      # for historic reasons)
   316      client.env.discard_option('tools-metadata-url')
   317      cloud_details = get_cloud_details(client, agent_metadata_url, agent_stream)
   318  
   319      with temp_yaml_file(cloud_details) as new_cloud:
   320          cloud_name = client.env.get_cloud()
   321          client.add_cloud(cloud_name, new_cloud)
   322          # Need to make sure we've refreshed any cache that we might have (as
   323          # this gets written to file during the bootstrap process.
   324          client.env.load_yaml()
   325          clouds = cloud_details['clouds'][cloud_name]
   326          assert_cloud_details_are_correct(client, cloud_name, clouds)
   327  
   328      client.generate_tool(agent_dir, agent_stream)
   329      set_new_log_dir(bs_manager, "assess_add_cloud")
   330  
   331      with bs_manager.booted_context(args.upload_tools):
   332          log.info('Metadata bootstrap successful.')
   333          verify_deployed_tool(agent_dir, client, agent_stream)
   334          log.info("Successfully deployed and verified add-cloud")
   335          deploy_charm_and_verify(client, "bionic", "dummy-source")
   336          log.info("Successfully deployed charm and verified")
   337  
   338  
   339  def clone_tgz_file_and_change_shasum(original_tgz_file, new_tgz_file):
   340      """
   341      Create a new tgz file from the original tgz file and then add empty file
   342      to it so that the sha256 sum of the new tgz file will be different from
   343      that of original tgz file. We use this to make sure that controller
   344      deployed on bootstrap used of the new tgz file.
   345      :param original_tgz_file: The source tgz file
   346      :param new_tgz_file: The destination tgz file
   347      """
   348      if not original_tgz_file.endswith(".tgz"):
   349          raise Exception("{} is not tgz file".format(original_tgz_file))
   350      try:
   351          command = "cat {}  <(echo -n ''| gzip)> {}".format(
   352              original_tgz_file, new_tgz_file)
   353          subprocess.Popen(command, shell=True, executable='/bin/bash')
   354      except subprocess.CalledProcessError as e:
   355          raise Exception("Failed to create a tool file {} - {}".format(
   356              original_tgz_file, e))
   357  
   358  
   359  @contextmanager
   360  def make_unique_tool(agent_file, agent_stream):
   361      """
   362      Create a tool directory for juju agent tools and stream and invoke
   363      clone_tgz_file_and_change_shasum function for the agent-file passed.
   364      :param agent_file: The agent file to use
   365      :param agent_stream: String representing agent stream name
   366      """
   367      with temp_dir() as agent_dir:
   368          juju_tool_src = os.path.join(agent_dir, "tools", agent_stream)
   369          os.makedirs(juju_tool_src)
   370          clone_tgz_file_and_change_shasum(agent_file, os.path.join(
   371              juju_tool_src, os.path.basename(agent_file)))
   372          log.debug("Created agent directory to perform bootstrap".format(
   373              agent_dir))
   374          yield agent_dir
   375  
   376  
   377  def parse_args(argv):
   378      """Parse all arguments."""
   379      parser = ArgumentParser(
   380          description="Test bootstrap for agent-metdadata-url")
   381  
   382      add_basic_testing_arguments(parser, existing=False)
   383  
   384      parser.add_argument('--agent-file', required=True, action='store',
   385                          help='agent file to be used during bootstrap.')
   386  
   387      return parser.parse_args(argv)
   388  
   389  
   390  def main(argv=None):
   391      args = parse_args(argv)
   392      configure_logging(args.verbose)
   393  
   394      if not os.path.isfile(args.agent_file):
   395          raise Exception(
   396              "Unable to find juju agent file {}".format(args.agent_file))
   397  
   398      agent_stream = args.agent_stream if args.agent_stream else 'testing'
   399  
   400      with make_unique_tool(args.agent_file, agent_stream) as agent_dir:
   401          assess_metadata(args, agent_dir, agent_stream)
   402  
   403      with make_unique_tool(args.agent_file, agent_stream) as agent_dir:
   404          assess_add_cloud(args, agent_dir, agent_stream)
   405  
   406      return 0
   407  
   408  
   409  if __name__ == '__main__':
   410      sys.exit(main())