def check(self): """ Check: Runs the check script. """ # 1. Load existing configuration state = self._load_state() if not self._load_state(): raise DisallowedOperationException( "Called check but found no state at %s. Call deploy first." % (self.state_file)) logger.debug("Loaded state: %s", state) network = self.client.network.get(state["network"]) if not network: raise DisallowedOperationException("Network %s not found!" % state["network"]) service = self.client.service.get(network, state["service"]) if not service: raise DisallowedOperationException("Service %s not found!" % state["service"]) instances = self.client.service.get_instances(service) if not instances: raise DisallowedOperationException( "No running instances in service %s!" % (state["service"])) if len(instances) > 1: raise DisallowedOperationException( "More than one running instance in service %s!" % (state["service"])) public_ip = instances[0].public_ip # 2. Run ./check <ssh_user> <ssh_key_path> return subprocess.run([ self.config.get_check_script_path(), state["ssh_username"], public_ip, self.private_key_path ], check=True)
def try_run(client, cmd): logger.info("running '%s'", cmd) _, stdout, stderr = client.exec_command(cmd) exit_status = stdout.channel.recv_exit_status() if exit_status: raise Exception( "Failed to delete image build user on image: %s. " "Exit code: %s." % (stderr.read(), exit_status)) logger.debug("Stdout: %s", stdout.read())
def from_file(cls, blueprint_filename): """ Loads the blueprint from a file. """ logger.debug("Creating blueprint from file: %s", blueprint_filename) blueprint_path = os.path.dirname(blueprint_filename) with open(blueprint_filename, 'r') as stream: blueprint = stream.read() return cls(blueprint, blueprint_path)
def __init__(self, blueprint, blueprint_path="./"): logger.debug("Creating blueprint from data: %s", blueprint) try: self.blueprint = yaml.safe_load(blueprint) except yaml.YAMLError as exc: logger.error("Error parsing blueprint: %s", exc) raise exc self.blueprint_path = blueprint_path if not self.blueprint: self.blueprint = {}
def load_config_for_cli(client, config): """ Try to load the configuration for an existing """ logger.debug("Running load_config_for_cli on: %s", config) config_obj = BlueprintTestConfiguration(config) state = get_state(config_obj) if not state: return None network = client.network.get(state["network_name"]) service = client.service.get(network, state["service_name"]) return (service, state["ssh_username"], private_key_path(config_obj))
def teardown(client, config): """ Destroy all services in this network, and destroy the network. """ logger.debug("Running teardown on: %s", config) config_obj = BlueprintTestConfiguration(config) state = get_state(config_obj) if not state or "network_name" not in state: return all_services = client.service.list() for service in all_services: if service.network.name == state["network_name"]: client.service.destroy(service) network = client.network.get(state["network_name"]) if network: client.network.destroy(network) save_state({}, config_obj) remove_key_pair(config_obj)
def verify(client, config): """ Verify that the instances are behaving as expected. """ logger.debug("Running verify on: %s", config) config_obj = BlueprintTestConfiguration(config) state = get_state(config_obj) blueprint_tester = get_blueprint_tester( client, config_obj.get_config_dir(), config_obj.get_verify_fixture_type(), config_obj.get_verify_fixture_options()) setup_info = SetupInfo(state["setup_info"]["deployment_info"], state["setup_info"]["blueprint_vars"]) network = client.network.get(state["network_name"]) service = client.service.get(network, state["service_name"]) blueprint_tester.verify(network, service, setup_info) logger.info("Verify successful!") return (service, state["ssh_username"], private_key_path(config_obj))
def call_with_retries(function, retry_count, retry_delay): """ Calls the given function with retries. Also handles logging on each retry. """ logger.debug("Calling function: %s with retry count: %s, retry_delay: %s", function, retry_count, retry_delay) retry = 0 while True: logger.info("Attempt number: %s", retry) retry = retry + 1 try: return function() # pylint: disable=broad-except except Exception as verify_exception: logger.info("Verify exception: %s", verify_exception) time.sleep(float(retry_delay)) if retry > int(retry_count): logger.info("Exceeded max retries! Reraising last exception") raise
def runtime_scripts(self, template_vars): """ Returns the contents of the provided runtime scripts. Currently only supports a list with one script. """ if not template_vars: template_vars = {} if len(self.blueprint["initialization"]) > 1: raise NotImplementedError("Only one initialization script currently supported") def handle_initialization_block(script): """ Handles a single initialization block. Factored out in case I want to support multiple startup scripts. """ full_path = os.path.join(self.blueprint_path, script["path"]) with open(full_path) as startup_script_file: startup_script = startup_script_file.read() template = jinja2.Template(startup_script) if "vars" in script: for name, opts in script["vars"].items(): if opts["required"] and name not in template_vars: raise BlueprintException( "Template Variable: \"%s\" must be set." % (name)) return template.render(template_vars) all_template_vars = [ name for initialization in self.blueprint["initialization"] if "vars" in initialization for name in initialization["vars"]] for template_var in template_vars: if template_var not in all_template_vars: raise BlueprintException( "Unrecognized Template Variable: \"%s\"." % (template_var)) rendered_runtime_scripts = handle_initialization_block(self.blueprint["initialization"][0]) logger.debug("Rendered runtime scripts: %s", rendered_runtime_scripts) return rendered_runtime_scripts
def get_fitting_instance(instances_client, blueprint): """ Finds the cheapest instance that satisfies the requirements specified in the given blueprint. """ # Raise exceptions for anything not supported if len(blueprint.disks()) > 1: raise NotImplementedError for disk in blueprint.disks(): if parse_storage_size(disk["size"]) > parse_storage_size("8GB"): raise NotImplementedError if disk["type"] != "standard": raise NotImplementedError if disk["device_name"] != "/dev/sda1": raise NotImplementedError # Get the smallest node type that satisfies our requirements node_types = instances_client.node_types() current_node = None for node_type in node_types: # First, check if the node even satisfies our requirements if (node_type["cpus"] >= blueprint.cpus() and node_type["memory"] >= blueprint.memory()): logger.debug("Need cpus: %s memory: %s", blueprint.cpus(), blueprint.memory()) logger.debug("Found satisfying node: %s", node_type) # Set our node to this one if it's the first we've found if not current_node: current_node = node_type # Otherwise, if this node is smaller than the one we found before, # reset (this is a heuristic for "cheapest"). elif (node_type["cpus"] <= current_node["cpus"] and node_type["memory"] <= current_node["memory"]): current_node = node_type return current_node["type"]
def _save(self, mock=False): """ Save: Saves the image after it is configured and checked (must run after configure/check) This is internal and not exposed on the command line because you should only save an image as part of a full build. All the other steps here are for debugging and development. """ # 1. Load existing configuration state = self._load_state() if not self._load_state(): raise DisallowedOperationException( "Called save but found no state at %s. Call deploy first." % (self.state_file)) logger.debug("Loaded state: %s", state) network = self.client.network.get(state["network"]) if not network: raise DisallowedOperationException("Network %s not found!" % state["network"]) service = self.client.service.get(network, state["service"]) if not service: raise DisallowedOperationException("Service %s not found!" % state["service"]) instances = self.client.service.get_instances(service) if not instances: raise DisallowedOperationException( "No running instances in service %s!" % (state["service"])) if len(instances) > 1: raise DisallowedOperationException( "More than one running instance in service %s!" % (state["service"])) public_ip = instances[0].public_ip # 2. Remove test keys if not mock: ssh = paramiko.SSHClient() ssh_key = paramiko.RSAKey(file_obj=open(state["ssh_private_key"])) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=public_ip, username=state["ssh_username"], pkey=ssh_key) # This removes all permissions from the temporary user's home directory and sets the # account to expire immediately. We unfortunately can't completely delete this # temporary account because we are currently logged in. def try_run(client, cmd): logger.info("running '%s'", cmd) _, stdout, stderr = client.exec_command(cmd) exit_status = stdout.channel.recv_exit_status() if exit_status: raise Exception( "Failed to delete image build user on image: %s. " "Exit code: %s." % (stderr.read(), exit_status)) logger.debug("Stdout: %s", stdout.read()) try_run(ssh, 'cd /tmp') try_run(ssh, 'sudo chmod -R 000 /home/%s/' % state["ssh_username"]) try_run(ssh, 'sudo usermod --expiredate 1 %s' % state["ssh_username"]) logger.info("Deleted test user: %s", state["ssh_username"]) ssh.close() # 3. Save the image with the correct name logger.debug("Saving service %s with name: %s", service.name, self.config.get_image_name()) image = self.client.image.create(self.config.get_image_name(), service) logger.info("Image saved with name: %s", self.config.get_image_name()) return image
def setup(client, config): """ Create all the boilerplate to spin up the service, and the service itself. """ logger.debug("Running setup to test: %s", config) config_obj = BlueprintTestConfiguration(config) state = get_state(config_obj) if state: raise DisallowedOperationException("Found non empty state file: %s" % state) network_name = generate_unique_name("test-network") service_name = generate_unique_name("test-service") key_pair = generate_ssh_keypair() state = { "network_name": network_name, "service_name": service_name, "public_key": key_pair.public_key, "private_key": key_pair.private_key } logger.debug("Saving state: %s now in case something fails", state) save_state(state, config_obj) save_key_pair(key_pair, config_obj) logger.debug("Creating test network: %s", network_name) network = client.network.create(network_name, NETWORK_BLUEPRINT) logger.debug("Calling the pre service setup in test fixture") blueprint_tester = get_blueprint_tester( client, config_obj.get_config_dir(), config_obj.get_create_fixture_type(), config_obj.get_create_fixture_options()) setup_info = blueprint_tester.setup_before_tested_service(network) if not isinstance(setup_info, SetupInfo): raise DisallowedOperationException( "Test fixture must return cloudless.testutils.fixture.SetupInfo object!" "Found: %s" % setup_info) state["setup_info"] = { "deployment_info": setup_info.deployment_info, "blueprint_vars": setup_info.blueprint_vars, } state["ssh_username"] = "******" logger.debug("Saving full state: %s", state) save_state(state, config_obj) # Add SSH key to the instance using reserved variables if "cloudless_test_framework_ssh_key" in setup_info.blueprint_vars: raise DisallowedOperationException( "cloudless_test_framework_ssh_key is a parameter reserved by the test framework " "and cannot be returned by the test fixture. Found: %s" % (setup_info.blueprint_vars)) setup_info.blueprint_vars[ "cloudless_test_framework_ssh_key"] = key_pair.public_key if "cloudless_test_framework_ssh_username" in setup_info.blueprint_vars: raise DisallowedOperationException( "cloudless_test_framework_ssh_username is a parameter reserved by the test " "framework and cannot be returned by the test fixture. Found: %s" % (setup_info.blueprint_vars)) setup_info.blueprint_vars["cloudless_test_framework_ssh_username"] = state[ "ssh_username"] logger.debug("Creating services using the blueprint under test") service = client.service.create(network, service_name, config_obj.get_blueprint_path(), setup_info.blueprint_vars, count=config_obj.get_count()) logger.debug("Calling the post service setup in test fixture") blueprint_tester.setup_after_tested_service(network, service, setup_info) logger.debug("Allowing SSH to test service") internet = CidrBlock("0.0.0.0/0") client.paths.add(internet, service, 22) logger.debug("Test service instances: %s", client.service.get_instances(service)) return (service, state["ssh_username"], private_key_path(config_obj))