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

     1  #!/usr/bin/env python
     2  """Functional tests for Cross Model Relation (CMR) functionality.
     3  
     4  This test exercises the CMR Juju functionality which allows applications to
     5  communicate between different models (including across controllers/clouds).
     6  
     7  The outline of this feature can be found here[1].
     8  
     9  This test will exercise the following aspects:
    10    - Ensure a user is able to create an offer of an applications' endpoint
    11      including:
    12        - A user is able to consume and relate to the offer
    13        - Workload data successfully provided
    14        - The offer appears in the list-offers output
    15        - The user is able to name the offer
    16        - The user is able to remove the offer
    17    - Ensure an admin can grant a user access to an offer
    18        - The consuming user finds the offer via 'find-offer'
    19  
    20  The above feature tests will be run on:
    21    - A single controller environment
    22    - Multiple controllers where each controller is in a different cloud.
    23  
    24  
    25  [1] https://docs.google.com/document/d/1IBTrqQSP3nrx5mTd_1vtUJ5YF28u9KJNTldUmUqrkJM/  # NOQA
    26  """
    27  
    28  from __future__ import print_function
    29  
    30  import argparse
    31  import logging
    32  import sys
    33  import yaml
    34  from textwrap import dedent
    35  
    36  from deploy_stack import (
    37      BootstrapManager,
    38      )
    39  from jujupy.client import (
    40      Controller,
    41      register_user_interactively,
    42      )
    43  from jujupy.models import (
    44      temporary_model
    45      )
    46  from jujucharm import local_charm_path
    47  from utility import (
    48      add_basic_testing_arguments,
    49      configure_logging,
    50      JujuAssertionError,
    51      )
    52  
    53  
    54  __metaclass__ = type
    55  
    56  
    57  log = logging.getLogger("assess_cross_model_relations")
    58  
    59  
    60  def assess_cross_model_relations_single_controller(args):
    61      """Assess that offers can be consumed in models on the same controller."""
    62      bs_manager = BootstrapManager.from_args(args)
    63      with bs_manager.booted_context(args.upload_tools):
    64          offer_model = bs_manager.client
    65          with temporary_model(offer_model, 'consume-model') as consume_model:
    66                  ensure_cmr_offer_management(offer_model)
    67                  ensure_cmr_offer_consumption_and_relation(
    68                      offer_model, consume_model)
    69  
    70  
    71  def assess_cross_model_relations_multiple_controllers(args):
    72      """Offers must be able to consume models on different controllers."""
    73      consume_bs_args = extract_second_provider_details(args)
    74      consume_bs_manager = BootstrapManager.from_args(consume_bs_args)
    75  
    76      offer_bs_manager = BootstrapManager.from_args(args)
    77      with offer_bs_manager.booted_context(args.upload_tools):
    78          offer_model = offer_bs_manager.client
    79          with consume_bs_manager.booted_context(consume_bs_args.upload_tools):
    80              consume_model = consume_bs_manager.client
    81              ensure_user_can_consume_offer(offer_model, consume_model)
    82  
    83  
    84  def ensure_cmr_offer_management(client):
    85      """Ensure creation, listing and deletion of offers work.
    86  
    87      Deploy dummy-source application onto `client` and offer it's endpoint.
    88      Ensure that:
    89        - The offer attempt is successful
    90        - The offer is shown in 'list-offers'
    91        - The offer can be deleted (and no longer appear in 'list-offers')
    92  
    93      :param client:   ModelClient used to create a new model and attempt 'offer'
    94        commands on
    95      """
    96      with temporary_model(client, 'offer-management') as management_model:
    97          app_name = 'dummy-source'
    98  
    99          deploy_local_charm(management_model, app_name)
   100  
   101          offer_url = assert_offer_is_listed(
   102              management_model, app_name, offer_name='kitchen-sink')
   103          assert_offer_can_be_deleted(management_model, offer_url)
   104  
   105          offer_url = assert_offer_is_listed(management_model, app_name)
   106          assert_offer_can_be_deleted(management_model, offer_url)
   107  
   108  
   109  def ensure_cmr_offer_consumption_and_relation(offer_client, consume_client):
   110      """Ensure offers can be consumed by another model.
   111  
   112      :param offer_client: ModelClient model that will be the source of the
   113        offer.
   114      :param consume_client: ModelClient model that will consume the offered
   115        application endpoint.
   116      """
   117      with temporary_model(offer_client, 'relation-source') as source_client:
   118          with temporary_model(consume_client, 'relation-sink') as sink_client:
   119              offer_url, offer_name = deploy_and_offer_db_app(source_client)
   120              assert_relating_to_offer_succeeds(sink_client, offer_url)
   121              assert_saas_url_is_correct(sink_client, offer_name, offer_url)
   122  
   123  
   124  def ensure_user_can_consume_offer(offer_client, consume_client):
   125      """Ensure admin is able to grant a user access to an offer.
   126  
   127      Almost the same as `ensure_cmr_offer_consumption_and_relation` except in
   128      this a user is created on all controllers (might be a single controller
   129      or 2) with the permissions of 'login' for the source controller and 'write'
   130      for the model into which the user will deploy an application to consume the
   131      offer.
   132  
   133      :param offer_client: ModelClient model that will be the source of the
   134        offer.
   135      :param consume_client: ModelClient model that will consume the offered
   136        application endpoint.
   137      """
   138      with temporary_model(offer_client, 'relation-source') as source_client:
   139          with temporary_model(consume_client, 'relation-sink') as sink_client:
   140              offer_url, offer_name = deploy_and_offer_db_app(source_client)
   141  
   142              username = 'theundertaker'
   143              token = source_client.add_user_perms(username)
   144              user_sink_client = register_user_on_controller(
   145                  sink_client, username, token)
   146  
   147              source_client.controller_juju(
   148                  'grant',
   149                  (username, 'consume', offer_url))
   150  
   151              offers_found = yaml.safe_load(
   152                  user_sink_client.get_juju_output(
   153                      'find-offers',
   154                      '--interface', 'mysql',
   155                      '--format', 'yaml',
   156                      include_e=False))
   157              # There must only be one offer
   158              user_offer_url = offers_found.keys()[0]
   159  
   160              assert_relating_to_offer_succeeds(sink_client, user_offer_url)
   161              assert_saas_url_is_correct(
   162                  sink_client, offer_name, user_offer_url)
   163  
   164  
   165  def assert_offer_is_listed(client, app_name, offer_name=None):
   166      """Assert that an offered endpoint is listed.
   167  
   168      :param client: ModelClient for model to use.
   169      :param app_name: Name of the deployed application to make an offer for.
   170      :param offer_name: If not None is used to name the endpoint offer.
   171      :return: String URL of the resulting offered endpoint.
   172      """
   173      log.info('Assessing {} offers.'.format(
   174          'named' if offer_name else 'unnamed'))
   175  
   176      expected_url, offer_key = offer_endpoint(
   177          client, app_name, 'sink', offer_name=offer_name)
   178      offer_output = yaml.safe_load(
   179          client.get_juju_output('offers', '--format', 'yaml'))
   180  
   181      fully_qualified_offer = '{controller}:{offer_url}'.format(
   182          controller=client.env.controller.name,
   183          offer_url=offer_output[offer_key]['offer-url'])
   184      try:
   185          if fully_qualified_offer != expected_url:
   186              raise JujuAssertionError(
   187                  'Offer URL mismatch.\n{actual} != {expected}'.format(
   188                      actual=offer_output[offer_key]['offer-url'],
   189                      expected=expected_url))
   190      except KeyError:
   191          raise JujuAssertionError('No offer URL found in offers output.')
   192  
   193      log.info('PASS: Assert offer is listed.')
   194      return expected_url
   195  
   196  
   197  def assert_offer_can_be_deleted(client, offer_url):
   198      """Assert that an offer can be successfully deleted."""
   199      client.juju('remove-offer', (offer_url), include_e=False)
   200      offer_output = yaml.safe_load(
   201          client.get_juju_output('offers', '--format', 'yaml'))
   202  
   203      if offer_output != {}:
   204          raise JujuAssertionError(
   205              'Failed to remove offer "{}"'.format(offer_url))
   206      log.info('PASS: Assert offer is removed.')
   207  
   208  
   209  def assert_relating_to_offer_succeeds(client, offer_url):
   210      """Deploy mediawiki on client and relate to provided `offer_url`.
   211  
   212      Raises an exception if the workload status does not move to 'active' within
   213      the default timeout (600 seconds).
   214      """
   215      client.deploy('cs:mediawiki')
   216      # No need to check workloads until the relation is set.
   217      client.wait_for_started()
   218      # mediawiki workload is blocked ('Database needed') until a db
   219      # relation is successfully made.
   220      client.juju('relate', ('mediawiki:db', offer_url))
   221      client.wait_for_workloads()
   222  
   223  
   224  def assert_saas_url_is_correct(client, offer_name, offer_url):
   225      """Offer url of Saas status field must match the expected `offer_url`."""
   226      status_saas_check = client.get_status()
   227      status_saas_url = status_saas_check.status[
   228          'application-endpoints'][offer_name]['url']
   229      if status_saas_url != offer_url:
   230          raise JujuAssertionError(
   231              'Consuming models status does not state status of the'
   232              'consumed offer.')
   233  
   234  
   235  def deploy_and_offer_db_app(client):
   236      """Deploy mysql application and offer it's db endpoint.
   237  
   238      :return: tuple of (resulting offer url, offer name)
   239      """
   240      client.deploy('cs:mysql')
   241      client.wait_for_started()
   242      client.wait_for_workloads()
   243      return offer_endpoint(client, 'mysql', 'db')
   244  
   245  
   246  def offer_endpoint(client, app_name, relation_name, offer_name=None):
   247      """Create an endpoint offer for `app_name` with optional name.
   248  
   249      :param client: ModelClient of model to operate on.
   250      :param app_name: Deployed application name to create offer for.
   251      :param offer_name: If not None create the offer with this name.
   252      :return: Tuple of the resulting offer url (including controller) and the
   253        offer name (default or named).
   254      """
   255      model_name = client.env.environment
   256      offer_endpoint = '{model}.{app}:{relation}'.format(
   257          model=model_name,
   258          app=app_name,
   259          relation=relation_name)
   260      offer_args = [offer_endpoint, '-c', client.env.controller.name]
   261      if offer_name:
   262          offer_args.append(offer_name)
   263      client.juju('offer', tuple(offer_args), include_e=False)
   264  
   265      offer_name = offer_name if offer_name else app_name
   266      offer_url = '{controller}:{user}/{model}.{offer}'.format(
   267          controller=client.env.controller.name,
   268          user=client.env.user_name,
   269          model=client.env.environment,
   270          offer=offer_name)
   271      return offer_url, offer_name
   272  
   273  
   274  def register_user_on_controller(client, username, token):
   275      """Register user with `token` on `client`s controller.
   276  
   277      :return: ModelClient object for the registered user.
   278      """
   279      controller_name = 'cmr_test'
   280      user_client = client.clone(env=client.env.clone())
   281      user_client.env.user_name = username
   282      user_client.env.controller = Controller(controller_name)
   283      register_user_interactively(user_client, token, controller_name)
   284      return user_client
   285  
   286  
   287  def deploy_local_charm(client, app_name):
   288      charm_path = local_charm_path(
   289          charm=app_name, juju_ver=client.version)
   290      client.deploy(charm_path)
   291      client.wait_for_started()
   292  
   293  
   294  def extract_second_provider_details(args):
   295      """Create a Namespace suitable for use with BootstrapManager.from_args.
   296  
   297      Using the 'secondary' environment details returns a argparse.Namespace
   298      object that can be used with BootstrapManager.from_args to get a
   299      bootstrap-able BootstrapManager.
   300      """
   301      new_args = vars(args).copy()
   302      new_args['env'] = new_args['secondary_env']
   303      new_args['region'] = new_args.get('secondary-region')
   304      new_args['temp_env_name'] = '{}-secondary'.format(
   305          new_args['temp_env_name'])
   306      return argparse.Namespace(**new_args)
   307  
   308  
   309  def parse_args(argv):
   310      parser = argparse.ArgumentParser(
   311          description="Cross Model Relations functional test.")
   312      parser.add_argument(
   313          '--secondary-env',
   314          help=dedent("""\
   315              The second provider to use for the test.
   316              If set the test will assess CMR functionality between the provider
   317              set in `primary-env` and this env (`secondary-env`).
   318              """))
   319      parser.add_argument(
   320          '--secondary-region',
   321          help='Override the default region for the secondary environment.')
   322      add_basic_testing_arguments(parser)
   323      return parser.parse_args(argv)
   324  
   325  
   326  def main(argv=None):
   327      args = parse_args(argv)
   328      configure_logging(args.verbose)
   329  
   330      assess_cross_model_relations_single_controller(args)
   331  
   332      if args.secondary_env:
   333          log.info('Assessing multiple controllers.')
   334          assess_cross_model_relations_multiple_controllers(args)
   335  
   336      return 0
   337  
   338  
   339  if __name__ == '__main__':
   340      sys.exit(main())