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