github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_endpoint_bindings.py (about) 1 #!/usr/bin/env python 2 """Validate endpoint bindings functionality on MAAS.""" 3 4 from __future__ import print_function 5 6 import argparse 7 import contextlib 8 import logging 9 import os 10 import shutil 11 import sys 12 import yaml 13 14 from deploy_stack import ( 15 BootstrapManager, 16 ) 17 from jujucharm import ( 18 Charm, 19 ) 20 from substrate import ( 21 maas_account_from_boot_config, 22 ) 23 from utility import ( 24 add_basic_testing_arguments, 25 configure_logging, 26 temp_dir, 27 ) 28 29 30 log = logging.getLogger("assess_endpoint_bindings") 31 32 33 script_identifier = "endpoint-bindings" 34 35 # To avoid clashes with other tests these space names must be seperately 36 # registered in jujupy to populate constraints. 37 space_data = script_identifier + "-data" 38 space_public = script_identifier + "-public" 39 40 41 def _generate_vids(start=10): 42 """ 43 Generate a series of vid values beginning with start. 44 45 Ideally these values would be carefully chosen to not clash with existing 46 vlans, but for now just hardcode. 47 """ 48 for vid in range(start, 4096): 49 yield vid 50 51 52 def _generate_cidrs(start=40, inc=10, block_pattern="10.0.{}.0/24"): 53 """ 54 Generate a series of cidrs based on block_pattern beginning with start. 55 56 Would be good not to hardcode but inspecting network for free ranges is 57 also non-trivial. 58 """ 59 for n in range(start, 255, inc): 60 yield block_pattern.format(n) 61 62 63 def ensure_spaces(manager, required_spaces): 64 """Return details for each given required_spaces creating spaces as needed. 65 66 :param manager: MAAS account manager. 67 :param required_spaces: List of space names that may need to be created. 68 """ 69 existing_spaces = manager.spaces() 70 log.info("Have spaces: %s", ", ".join(s["name"] for s in existing_spaces)) 71 spaces_map = dict((s["name"], s) for s in existing_spaces) 72 spaces = [] 73 for space_name in required_spaces: 74 space = spaces_map.get(space_name) 75 if space is None: 76 space = manager.create_space(space_name) 77 log.info("Created space: %r", space) 78 spaces.append(space) 79 return spaces 80 81 82 @contextlib.contextmanager 83 def reconfigure_networking(manager, required_spaces): 84 """Create new MAAS networking primitives to prepare for testing. 85 86 :param manager: MAAS account manager. 87 :param required_spaces: List of spaces to make with vlans and subnets. 88 """ 89 new_subnets = [] 90 new_vlans = [] 91 fabrics = manager.fabrics() 92 log.info("Have fabrics: %s", ", ".join(f["name"] for f in fabrics)) 93 new_fabric = manager.create_fabric(script_identifier) 94 try: 95 log.info("Created fabric: %r", new_fabric) 96 97 spaces = ensure_spaces(manager, required_spaces) 98 99 for vid, space_name in zip(_generate_vids(), required_spaces): 100 name = space_name + "-vlan" 101 new_vlans.append(manager.create_vlan(new_fabric["id"], vid, name)) 102 log.info("Created vlan: %r", new_vlans[-1]) 103 104 for cidr, vlan, space in zip(_generate_cidrs(), new_vlans, spaces): 105 new_subnets.append(manager.create_subnet( 106 cidr, fabric_id=new_fabric["id"], vlan_id=vlan["id"], 107 space=space["id"], gateway_ip=cidr.replace(".0/24", ".1"))) 108 log.info("Created subnet: %r", new_subnets[-1]) 109 110 yield new_fabric, spaces, list(new_vlans), list(new_subnets) 111 112 finally: 113 for subnet in new_subnets: 114 manager.delete_subnet(subnet["id"]) 115 log.info("Deleted subnet: %s", subnet["name"]) 116 117 for vlan in new_vlans: 118 manager.delete_vlan(new_fabric["id"], vlan["vid"]) 119 log.info("Deleted vlan: %s", vlan["name"]) 120 121 try: 122 manager.delete_fabric(new_fabric["id"]) 123 except Exception: 124 log.exception("Failed to delete fabric: %s", new_fabric["id"]) 125 else: 126 log.info("Deleted fabric: %s", new_fabric["id"]) 127 128 129 @contextlib.contextmanager 130 def reconfigure_machines(manager, fabric, required_machine_subnets): 131 """ 132 Reconfigure MAAS machines with new interfaces to prepare for testing. 133 134 There are some unavoidable races if multiple jobs attempt to reconfigure 135 machines at the same time. Also, in heterogenous environments an 136 inadequate machine may be reserved at this time. 137 138 Ideally this function would just allocate some machines before operating 139 on them. Alas, MAAS doesn't allow interface changes on allocated machines 140 and Juju will not select them for deployment. 141 142 :param manager: MAAS account manager. 143 :param fabric: Data from MAAS about the fabric to be used. 144 :param required_machine_subnets: List of list of vlan and subnet ids. 145 """ 146 147 # Find all machines not currently being used 148 all_machines = manager.machines() 149 candidate_machines = [ 150 m for m in all_machines if m["status"] == manager.STATUS_READY] 151 # Take the id of the default vlan on the new fabric 152 default_vlan = fabric["vlans"][0]["id"] 153 154 configured_machines = [] 155 machine_interfaces = {} 156 try: 157 for machine_subnets in required_machine_subnets: 158 if not candidate_machines: 159 raise Exception("No ready maas machines to configure") 160 161 machine = candidate_machines.pop() 162 system_id = machine["system_id"] 163 # TODO(gz): Add logic to pick sane parent? 164 existing_interface = [ 165 interface for interface in machine["interface_set"] 166 if not any("subnet" in link for link in interface["links"]) 167 ][0] 168 previous_vlan_id = existing_interface["vlan"]["id"] 169 new_interfaces = [] 170 machine_interfaces[system_id] = ( 171 existing_interface, previous_vlan_id, new_interfaces) 172 manager.interface_update( 173 system_id, existing_interface["id"], vlan_id=default_vlan) 174 log.info("Changed existing interface: %s %s", system_id, 175 existing_interface["name"]) 176 parent = existing_interface["id"] 177 178 for vlan_id, subnet_id in machine_subnets: 179 links = [] 180 interface = manager.interface_create_vlan( 181 system_id, parent, vlan_id) 182 new_interfaces.append(interface) 183 log.info("Created interface: %r", interface) 184 185 updated_subnet = manager.interface_link_subnet( 186 system_id, interface["id"], "AUTO", subnet_id) 187 # TODO(gz): Need to pick out right link if multiple are added. 188 links.append(updated_subnet["links"][0]) 189 log.info("Created link: %r", links[-1]) 190 191 configured_machines.append(machine) 192 yield configured_machines 193 finally: 194 log.info("About to reset machine interfaces to original states.") 195 for system_id in machine_interfaces: 196 parent, vlan, children = machine_interfaces[system_id] 197 for child in children: 198 manager.delete_interface(system_id, child["id"]) 199 log.info("Deleted interface: %s %s", system_id, child["id"]) 200 manager.interface_update(system_id, parent["id"], vlan_id=vlan) 201 log.info("Reset original interface: %s", parent["name"]) 202 203 204 def create_test_charms(): 205 """Create charms for testing and bundle using them.""" 206 charm_datastore = Charm("datastore", "Testing datastore charm.") 207 charm_datastore.metadata["provides"] = { 208 "datastore": {"interface": "data"}, 209 } 210 211 charm_frontend = Charm("frontend", "Testing frontend charm.") 212 charm_frontend.metadata["extra-bindings"] = { 213 "website": None, 214 } 215 charm_frontend.metadata["requires"] = { 216 "datastore": {"interface": "data"}, 217 } 218 219 bundle = { 220 "machines": { 221 "0": { 222 "constraints": "spaces={},^{}".format( 223 space_data, space_public), 224 "series": "bionic", 225 }, 226 "1": { 227 "constraints": "spaces={},{}".format(space_data, space_public), 228 "series": "bionic", 229 }, 230 "2": { 231 "constraints": "spaces={},{}".format(space_data, space_public), 232 "series": "bionic", 233 }, 234 }, 235 "services": { 236 "datastore": { 237 "charm": "./bionic/datastore", 238 "series": "bionic", 239 "num_units": 1, 240 "to": ["0"], 241 "bindings": { 242 "datastore": space_data, 243 }, 244 }, 245 "frontend": { 246 "charm": "./bionic/frontend", 247 "series": "bionic", 248 "num_units": 1, 249 "to": ["1"], 250 "bindings": { 251 "website": space_public, 252 "datastore": space_data, 253 }, 254 }, 255 "monitor": { 256 "charm": "./bionic/datastore", 257 "series": "bionic", 258 "num_units": 1, 259 "to": ["2"], 260 "bindings": { 261 "": space_data, 262 }, 263 }, 264 }, 265 "relations": [ 266 ["datastore:datastore", "frontend:datastore"], 267 ], 268 } 269 return bundle, [charm_datastore, charm_frontend] 270 271 272 @contextlib.contextmanager 273 def using_bundle_and_charms(bundle, charms, bundle_name="bundle.yaml"): 274 """Commit bundle and charms to disk and gives path to bundle.""" 275 with temp_dir() as working_dir: 276 for charm in charms: 277 charm.to_repo_dir(working_dir) 278 279 # TODO(gz): Create a bundle abstration in jujucharm module 280 bundle_path = os.path.join(working_dir, bundle_name) 281 with open(bundle_path, "w") as f: 282 yaml.safe_dump(bundle, f) 283 284 yield bundle_path 285 286 287 def machine_spaces_for_bundle(bundle): 288 """Return a list of sets of spaces required for machines in bundle.""" 289 machines = [] 290 for service in bundle["services"].values(): 291 spaces = frozenset(service.get("bindings", {}).values()) 292 num_units = service.get("num_units", 1) 293 machines.extend([spaces] * num_units) 294 return machines 295 296 297 def bootstrap_and_test(bootstrap_manager, bundle_path, machine): 298 shutil.copy(bundle_path, bootstrap_manager.log_dir) 299 with bootstrap_manager.booted_context(False, no_gui=True): 300 client = bootstrap_manager.client 301 log.info("Deploying bundle.") 302 client.deploy(bundle_path) 303 log.info("Waiting for all units to start.") 304 client.wait_for_started() 305 client.wait_for_workloads() 306 log.info("Validating bindings.") 307 validate(client) 308 309 310 def validate(client): 311 """Ensure relations are bound to the correct spaces.""" 312 313 314 def assess_endpoint_bindings(maas_manager, bootstrap_manager): 315 required_spaces = [space_data, space_public] 316 317 bundle, charms = create_test_charms() 318 319 machine_spaces = machine_spaces_for_bundle(bundle) 320 # Add a bootstrap machine in all spaces 321 machine_spaces.insert(0, frozenset().union(*machine_spaces)) 322 323 log.info("About to write charms to disk.") 324 with using_bundle_and_charms(bundle, charms) as bundle_path: 325 log.info("About to reconfigure maas networking.") 326 with reconfigure_networking(maas_manager, required_spaces) as nets: 327 328 fabric, spaces, vlans, subnets = nets 329 # Derive the vlans and subnets that need to be added to machines 330 vlan_subnet_per_machine = [] 331 for spaces in machine_spaces: 332 idxs = sorted(required_spaces.index(space) for space in spaces) 333 vlans_subnets = [ 334 (vlans[i]["id"], subnets[i]["id"]) for i in idxs] 335 vlan_subnet_per_machine.append(vlans_subnets) 336 337 log.info("About to add new interfaces to machines.") 338 with reconfigure_machines( 339 maas_manager, fabric, vlan_subnet_per_machine) as machines: 340 341 bootstrap_manager.client.use_reserved_spaces(required_spaces) 342 343 base_machine = machines[0]["hostname"] 344 345 log.info("About to bootstrap.") 346 bootstrap_and_test( 347 bootstrap_manager, bundle_path, base_machine) 348 349 350 def parse_args(argv): 351 """Parse all arguments.""" 352 parser = argparse.ArgumentParser(description="assess endpoint bindings") 353 add_basic_testing_arguments(parser, existing=False) 354 args = parser.parse_args(argv) 355 if args.upload_tools: 356 parser.error("giving --upload-tools meaningless on 2.0 only test") 357 return args 358 359 360 def main(argv=None): 361 args = parse_args(argv) 362 configure_logging(args.verbose) 363 bs_manager = BootstrapManager.from_args(args) 364 with maas_account_from_boot_config(bs_manager.client.env) as account: 365 assess_endpoint_bindings(account, bs_manager) 366 return 0 367 368 369 if __name__ == '__main__': 370 sys.exit(main())