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