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

     1  #!/usr/bin/env python
     2  
     3  from argparse import ArgumentParser
     4  from collections import namedtuple
     5  from copy import deepcopy
     6  import logging
     7  import re
     8  import sys
     9  
    10  import yaml
    11  
    12  from jujupy import (
    13      ModelClient,
    14      JujuData,
    15      )
    16  from jujupy.exceptions import (
    17      AuthNotAccepted,
    18      InvalidEndpoint,
    19      NameNotAccepted,
    20      TypeNotAccepted,
    21  )
    22  from utility import (
    23      add_arg_juju_bin,
    24      JujuAssertionError,
    25      temp_dir,
    26      )
    27  
    28  
    29  # URLs are limited to 2083 bytes in many browsers, anything more is excessive.
    30  # Juju has set 4096 as being excessive, but it needs to be lowered
    31  # https://bugs.launchpad.net/juju/+bug/1678833
    32  EXCEEDED_LIMIT = 4096
    33  
    34  
    35  class CloudMismatch(JujuAssertionError):
    36      """The clouds did not match in some way."""
    37  
    38      def __init__(self):
    39          super(CloudMismatch, self).__init__('Cloud mismatch')
    40  
    41  
    42  class NameMismatch(JujuAssertionError):
    43      """The cloud names did not match."""
    44  
    45      def __init__(self):
    46          super(NameMismatch, self).__init__('Name mismatch')
    47  
    48  
    49  class NotRaised(Exception):
    50      """An expected exception was not raised."""
    51  
    52      def __init__(self, cloud_spec):
    53          msg = 'Expected exception not raised: {}'.format(
    54              cloud_spec.exception)
    55          super(NotRaised, self).__init__(msg)
    56  
    57  
    58  class CloudValidation:
    59  
    60      NONE = object
    61      BASIC = object()
    62      ENDPOINT = object()
    63  
    64      def __init__(self, version):
    65          """Initialize with the juju version."""
    66          self.version = version
    67          if re.match('2\.0[^\d]', version):
    68              self.support = self.NONE
    69          elif re.match('2\.1[^\d]', version):
    70              self.support = self.BASIC
    71          else:
    72              # re.match('2\.2[^\d]', version)
    73              # 2.2 retracted manual endpoint validation because it is entangled
    74              # with authentication.
    75              self.support = self.ENDPOINT
    76  
    77      @property
    78      def is_basic(self):
    79          return self.support is self.BASIC
    80  
    81      @property
    82      def is_endpoint(self):
    83          return self.support is self.ENDPOINT
    84  
    85      def has_endpoint(self, provider):
    86          """Return True if the juju provider supports endpoint validation.
    87  
    88          :param provider: The cloud provider type.
    89          """
    90          if self.support is self.ENDPOINT and provider != 'manual':
    91              return True
    92          return False
    93  
    94  
    95  CloudSpec = namedtuple('CloudSpec', [
    96      'label', 'name', 'config', 'exception', 'xfail_bug'])
    97  
    98  
    99  def cloud_spec(label, name, config, exception=None, xfail_bug=None):
   100      """Generate a CloudSpec, with defaults.
   101  
   102      :param label: The label to display in test results.
   103      :param name: The name to use for the cloud.
   104      :param config: The cloud-config.
   105      :param exception: The exception that is expected to be raised (if any).
   106      :param xfail_bug: If this CloudSpec represents an expected failure, the
   107          bug number.
   108      """
   109      return CloudSpec(label, name, config, exception, xfail_bug)
   110  
   111  
   112  def xfail(spec, bug, xfail_exception):
   113      """Return a variant of a CloudSpec that is expected to fail.
   114  
   115      Wrapping the original spec improves maintainability, because the xfail can
   116      be removed to restore the original value.
   117      """
   118      return CloudSpec(spec.label, spec.name, spec.config, xfail_exception, bug)
   119  
   120  
   121  def assess_cloud(client, cloud_name, example_cloud):
   122      """Assess interactively adding a cloud.
   123  
   124      Will raise an exception
   125      - If no clouds are present after interactive add-cloud.
   126      - If the resulting cloud name doesn't match the supplied cloud-name.
   127      - If the cloud data doesn't match the supplied cloud data.
   128      """
   129      clouds = client.env.read_clouds()
   130      if len(clouds['clouds']) > 0:
   131          raise AssertionError('Clouds already present!')
   132      client.add_cloud_interactive(cloud_name, example_cloud)
   133      clouds = client.env.read_clouds()
   134      if len(clouds['clouds']) == 0:
   135          raise JujuAssertionError('Clouds missing!')
   136      if clouds['clouds'].keys() != [cloud_name]:
   137          raise NameMismatch()
   138      if clouds['clouds'][cloud_name] != example_cloud:
   139          sys.stderr.write('\nExpected:\n')
   140          yaml.dump(example_cloud, sys.stderr)
   141          sys.stderr.write('\nActual:\n')
   142          yaml.dump(clouds['clouds'][cloud_name], sys.stderr)
   143          raise CloudMismatch()
   144  
   145  
   146  def iter_clouds(clouds, cloud_validation):
   147      """Iterate through CloudSpecs.
   148  
   149      :param clouds: cloud data as defined in $JUJU_DATA/clouds.yaml
   150      :param cloud_validation: an instance of CloudValidation.
   151      """
   152      yield cloud_spec('bogus-type', 'bogus-type', {'type': 'bogus'},
   153                       exception=TypeNotAccepted)
   154      for cloud_name, cloud in clouds.items():
   155          spec = cloud_spec(cloud_name, cloud_name, cloud)
   156          yield spec
   157  
   158      long_text = 'A' * EXCEEDED_LIMIT
   159  
   160      for cloud_name, cloud in clouds.items():
   161          spec = xfail(cloud_spec('long-name-{}'.format(cloud_name), long_text,
   162                                  cloud, NameNotAccepted), 1641970, NameMismatch)
   163          yield spec
   164          spec = xfail(
   165              cloud_spec('invalid-name-{}'.format(cloud_name), 'invalid/name',
   166                         cloud, NameNotAccepted), 1641981, None)
   167          yield spec
   168  
   169          if cloud['type'] not in ('maas', 'manual', 'vsphere'):
   170              variant = deepcopy(cloud)
   171              variant_name = 'bogus-auth-{}'.format(cloud_name)
   172              variant['auth-types'] = ['asdf']
   173              yield cloud_spec(variant_name, cloud_name, variant,
   174                               AuthNotAccepted)
   175  
   176          if 'endpoint' in cloud:
   177              variant = deepcopy(cloud)
   178              variant['endpoint'] = long_text
   179              if variant['type'] == 'vsphere':
   180                  for region in variant['regions'].values():
   181                      region['endpoint'] = variant['endpoint']
   182              variant_name = 'long-endpoint-{}'.format(cloud_name)
   183              spec = cloud_spec(variant_name, cloud_name, variant,
   184                                InvalidEndpoint)
   185              if not cloud_validation.has_endpoint(cloud['type']):
   186                  spec = xfail(spec, 1641970, CloudMismatch)
   187              yield spec
   188  
   189          for region_name in cloud.get('regions', {}).keys():
   190              if cloud['type'] == 'vsphere':
   191                  continue
   192              variant = deepcopy(cloud)
   193              region = variant['regions'][region_name]
   194              region['endpoint'] = long_text
   195              variant_name = 'long-endpoint-{}-{}'.format(cloud_name,
   196                                                          region_name)
   197              spec = cloud_spec(variant_name, cloud_name, variant,
   198                                InvalidEndpoint)
   199              if not cloud_validation.has_endpoint(cloud['type']):
   200                  spec = xfail(spec, 1641970, CloudMismatch)
   201              yield spec
   202  
   203  
   204  def assess_all_clouds(client, cloud_specs):
   205      """Test all the supplied cloud_specs and return the results.
   206  
   207      Returns a tuple of succeeded, expected_failed, and failed.
   208      succeeded and failed are sets of cloud labels.  expected_failed is a dict
   209      linking a given bug to its associated failures.
   210      """
   211      succeeded = set()
   212      xfailed = {}
   213      failed = set()
   214      client.env.load_yaml()
   215      for cloud_spec in cloud_specs:
   216          sys.stdout.write('Testing {}.\n'.format(cloud_spec.label))
   217          try:
   218              if cloud_spec.exception is None:
   219                  assess_cloud(client, cloud_spec.name, cloud_spec.config)
   220              else:
   221                  try:
   222                      assess_cloud(client, cloud_spec.name, cloud_spec.config)
   223                  except cloud_spec.exception:
   224                      pass
   225                  else:
   226                      raise NotRaised(cloud_spec)
   227          except Exception as e:
   228              logging.exception(e)
   229              failed.add(cloud_spec.label)
   230          else:
   231              if cloud_spec.xfail_bug is not None:
   232                  xfailed.setdefault(
   233                      cloud_spec.xfail_bug, set()).add(cloud_spec.label)
   234              else:
   235                  succeeded.add(cloud_spec.label)
   236          finally:
   237              client.env.clouds = {'clouds': {}}
   238              client.env.dump_yaml(client.env.juju_home)
   239      return succeeded, xfailed, failed
   240  
   241  
   242  def write_status(status, tests):
   243      if len(tests) == 0:
   244          test_str = 'none'
   245      else:
   246          test_str = ', '.join(sorted(tests))
   247      sys.stdout.write('{}: {}\n'.format(status, test_str))
   248  
   249  
   250  def parse_args():
   251      parser = ArgumentParser()
   252      parser.add_argument('example_clouds',
   253                          help='A clouds.yaml file to use for testing.')
   254      add_arg_juju_bin(parser)
   255      return parser.parse_args()
   256  
   257  
   258  def main():
   259      args = parse_args()
   260      juju_bin = args.juju_bin
   261      version = ModelClient.get_version(juju_bin)
   262      with open(args.example_clouds) as f:
   263          clouds = yaml.safe_load(f)['clouds']
   264      cloug_validation = CloudValidation(version)
   265      cloud_specs = iter_clouds(clouds, cloug_validation)
   266      with temp_dir() as juju_home:
   267          env = JujuData('foo', config=None, juju_home=juju_home)
   268          client = ModelClient(env, version, juju_bin)
   269          succeeded, xfailed, failed = assess_all_clouds(client, cloud_specs)
   270      write_status('Succeeded', succeeded)
   271      for bug, failures in sorted(xfailed.items()):
   272          write_status('Expected fail (bug #{})'.format(bug), failures)
   273      write_status('Failed', failed)
   274      if len(failed) > 0:
   275          return 1
   276      return 0
   277  
   278  
   279  if __name__ == '__main__':
   280      sys.exit(main())