def __init__(self, config, pyrax): self.pyrax = pyrax self.config = config self.as_config = config.get_autoscale_config() # Launch config as read from the config file self.lc_config = config.get_launch_config() self.autoscale = self.pyrax.autoscale self.scaling_group = None self.scale_up_policy = None self.scale_down_policy = None # Launch config set on the scaling group self.launch_config = None self.group_id = self.as_config.id if not self.group_id: self.create_group() self.launch_config = self.scaling_group.get_launch_config() print_msg("Created - %s - %s " % (self.scaling_group.name, self.scaling_group.id), bcolors.OKGREEN) else: self.scaling_group = self.autoscale.get(self.group_id) self.launch_config = self.scaling_group.get_launch_config() diffs = self.diff_group() if self.check_and_confirm_change(diffs): self.update_group(diffs.get('scaling_group', None)) self.update_launch_config(diffs.get('launch_config', None)) self.update_policies(diffs)
def ask_str(msg, allowed_input=None, yesno=False): """ Prompts for input, and ensures input is acceptable. allowed_input is a list containing expected inputs. Optionally set yesno to True, and this function will be set up to expect input to a yes or no question and return a string containing 'yes' for a positive answer and None to a negative one. """ if yesno: positive = ['yes', 'y'] negative = ['no', 'n'] allowed_input = positive + negative while True: ret = raw_input(bcolors.QUESTION + msg + bcolors.ENDC) if yesno and ret.lower() in allowed_input: if ret.lower() in positive: return 'yes' elif ret.lower() in negative: return None else: print_msg("Answer not valid", bcolors.FAIL) continue elif allowed_input and ret in allowed_input: return ret elif not allowed_input: return ret else: print_msg("Answer not valid", bcolors.FAIL)
def ask_file(msg): while True: file_name = ask_str(msg) if not is_readable(file_name): print_msg("Unable to open file %s for reading, please try again" % file_name, bcolors.FAIL) else: return file_name
def get_user_data_from_file(self): file_name = self.lc_config.cloud_init if not file_name: return None if not utils.is_readable(file_name): print_msg("Can't open cloud-init file %s for reading" % file_name, bcolors.FAIL) return open(file_name, 'r').read()
def diff_scale_down_policy(self): diff_found = False policy = self.get_scale_down_policy() if int(policy.change) != int(self.as_config.scale_down): print_msg("Difference detected in key scale_down: %s != %s" % ( policy.change, self.as_config.scale_down), bcolors.FAIL) diff_found = True return diff_found
def update_group(self, diffs): if not diffs: return try: self.scaling_group.update(name=self.as_config.name, cooldown=self.as_config.cooldown, min_entities=self.as_config.min_entities, max_entities=self.as_config.max_entities) print_msg("Group successfully updated", bcolors.OKGREEN) except Exception as ex: print_msg("Failed to update group - %s" % ex, bcolors.FAIL)
def ask_integer(msg, allowed_input=None): """ Prompts for input, and ensures input is an integer """ while True: try: ret = int(raw_input(bcolors.QUESTION + msg + bcolors.ENDC)) if allowed_input and ret in allowed_input: return ret elif allowed_input and ret not in allowed_input: print_msg("Answer not in range", bcolors.FAIL) else: return ret except ValueError: print_msg("Value must be a number", bcolors.FAIL)
def add_new_key(pyrax): name = None while True: public_name = ask_str("Path to public key file: ") name = raw_input("Keypair name: ") try: with open(os.path.expanduser(public_name)) as keyfile: pyrax.cloudservers.keypairs.create(name, keyfile.read()) break except IOError as ex: print_msg("Unable to read file: %s Please try again..." % ex, bcolors.FAIL) except novaclient.exceptions.Conflict: print_msg("A key with that name already exists", bcolors.FAIL) return name
def diff_group(self): """ Compares an existing group with the config variables. Returns a tuple of dicts containing the parameters that are different in the scaling group and launch configuration from what's defined in the config file or None if they match """ diffs = {} diffs['scaling_group'] = self.diff_autoscale() diffs['scale_up_policy'] = self.diff_scale_up_policy() diffs['scale_down_policy'] = self.diff_scale_down_policy() diffs['launch_config'] = self.diff_launch_config() if any(k[1] for k in diffs.iteritems()): return diffs print_msg("Running scaling group config matches" " that of config file...", bcolors.OKGREEN) return None
def diff_autoscale(self): """ Compare autoscale configuration from file with what's on current group. scale_up and scale_down are actually policy properties, not scaling group. So we handle those separately """ diff_found = False autoscale_keys = [key for key in self.config.get_keys('autoscale') if not key.startswith('scale_')] for key in autoscale_keys: if getattr(self.scaling_group, key) !=\ getattr(self.as_config, key): print_msg("Difference detected in key %s: %s != %s" % (key, getattr( self.scaling_group, key), getattr(self.as_config, key)), bcolors.FAIL) diff_found = True return diff_found
def parse_credentials(self, config_file=None): if self.username and self.api_key and self.region: return config_file = config_file if config_file else self.config_file self.read_config(config_file) # We need to be able to read both our config as well as a pyrax one section = 'rackspace_cloud' if self.cfg.has_section('rackspace_cloud')\ else 'cloud' # First check whether credentials are specified explicitly try: self.username = self.cfg.get(section, 'username').strip("'") self.api_key = self.cfg.get(section, 'api_key').strip("'") self.region = self.cfg.get(section, 'region').strip("'") # We don't want to write these out to the config file... self.cfg.remove_section(section) return except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): # Ignore if they aren't pass # And check for a credentials_file key, and re-parse using that file # if found. If it isn't, bomb out, we have no credentials try: self.credentials_file = ast.literal_eval(self.cfg.get(section, 'credentials_file')) self.parse_credentials(self.credentials_file) except ConfigParser.NoSectionError: print_msg("Config file %s does not contain a 'cloud' or" " 'rackspace_cloud' section" % config_file, bcolors.FAIL) exit(1) except ConfigParser.NoOptionError: print_msg("Config file %s does not contain the keys username," " api_key, region or credentials_file" % config_file, bcolors.FAIL) exit(1) self.cfg.remove_section(section)
def diff_launch_config(self): diff_found = False for key in self.launch_config: if key == 'name': if str(self.launch_config.get(key)) != \ str(getattr(self.lc_config, key)): print_msg("Difference detected in key name in section" " 'launch-configuration': %s != %s" % ( self.launch_config.get(key), getattr(self.lc_config, key)), bcolors.FAIL) diff_found = True elif key == 'load_balancers': # We don't let Autoscale manage load balancers for us pass elif key == 'user_data': if getattr(self.lc_config, (key)) \ != utils.unb64(self.launch_config.get(key)): print_msg("Difference detected in key user_data in section" " launch-configuration' (new config at the" " bottom):", bcolors.FAIL) ud_diffs = difflib.context_diff( utils.unb64(self.launch_config.get(key)).splitlines(), getattr(self.lc_config, (key)).splitlines()) for a in ud_diffs: print a diff_found = True else: if self.launch_config.get(key) != getattr(self.lc_config, key): print_msg("Difference detected in key %s in section" " 'launch-configuration': %s != %s" % ( key, self.launch_config.get(key), getattr(self.lc_config, key)), bcolors.FAIL) diff_found = True return diff_found
def generate_rax_as_config(config): input_template = os.getcwd() + '/templates/rax-autoscaler.json.j2' output_file = os.getcwd() + '/rax-autoscaler-config.json' try: with open(input_template, 'r') as fp: j2_env = Environment().from_string(fp.read()) except IOError as ex: print_msg("Failed to open rax-autoscaler config template: %s" % ex, bcolors.FAIL) exit(1) scale_up_policy = config.cfg.get('rax-autoscaler', 'scale_up_policy').strip("'") scale_down_policy = config.cfg.get('rax-autoscaler', 'scale_down_policy').strip("'") load_balancers = config.cfg.get('rax-autoscaler', 'load_balancers').strip("'") autoscale_group = config.as_config.id.strip("'") num_static_servers = config.cfg.get('rax-autoscaler', 'num_static_servers') t = j2_env.render(username=config.username, api_key=config.api_key, region=config.region, autoscale_group=autoscale_group, scale_up_policy=scale_up_policy, scale_down_policy=scale_down_policy, load_balancers=load_balancers, num_static_servers=num_static_servers) try: with open(output_file, 'w+') as fp: fp.write(t) print_msg("Wrote rax-autoscaler to file %s" % output_file, bcolors.OKGREEN) except IOError as ex: print_msg("Failed to write rax-autoscaler config: %s" % ex, bcolors.FAIL)
def write_config(config, pyrax): """ Prompt for missing keys in the config file and writes a new one out """ config.parse_config() try: if config.lc_config.validate() and \ config.as_config.validate() and \ config.as_config.id: utils.print_msg( "Config defined in %s passes validation." " Checking for misconfiguration and missing" " optional keys.." % (config.config_file), bcolors.OKGREEN) except AttributeError: pass ## # Write [autoscale] config ## if not config.as_config.id: group = utils.get_object_from_list(pyrax.autoscale, "group", create_new_option=True) if group is not None: config.set_config_option('autoscale', 'id', group) if not config.as_config.name: group_name = utils.ask_str("Name of autoscale_group: ") config.set_config_option('autoscale', 'name', group_name) if not isinstance(config.as_config.scale_up, int): scale_up = utils.ask_integer( "Number of servers to scale up by when triggered: ") config.set_config_option('autoscale', 'scale_up', scale_up) if not isinstance(config.as_config.scale_down, int): scale_down = utils.ask_integer( "Number of servers to scale down by when triggered: ") config.set_config_option('autoscale', 'scale_down', scale_down) if not isinstance(config.as_config.max_entities, int): max_entities = utils.ask_integer( "Max number of servers to scale up to (max_entities): ") config.set_config_option('autoscale', 'max_entities', max_entities) if not isinstance(config.as_config.min_entities, int): max_entities = config.as_config.max_entities min_entities = utils.ask_integer( "Never scale down below this number of servers (min_entities): ") if min_entities > max_entities: print_msg( "min_entities must be smaller than or equal" "to max_entities (%d)" % max_entities, bcolors.FAIL) min_entities = utils.ask_integer( "Never scale down below this " "number of servers" " (min_entities): ", allowed_input=xrange(0, max_entities)) config.set_config_option('autoscale', 'min_entities', min_entities) if not isinstance(config.as_config.cooldown, int): cooldown = utils.ask_integer( "Do not process scale event more frequent than" " this (cooldown, seconds): ") config.set_config_option('autoscale', 'cooldown', cooldown) ## # Write [launch-config] config ## if not config.lc_config.image: image = utils.get_object_from_list(pyrax.images, "image") config.set_config_option('launch-configuration', 'image', image) if not config.lc_config.flavor: flavor = utils.get_object_from_list(pyrax.cloudservers.flavors, "flavor") config.set_config_option('launch-configuration', 'flavor', flavor) if not config.lc_config.key_name: key_name = utils.get_object_from_list(pyrax.cloudservers.keypairs, "ssh-key to add to" " /root/.ssh/authorized_keys on" " the servers", create_new_option=True) if key_name is None: key_name = utils.add_new_key(pyrax) config.set_config_option('launch-configuration', 'key_name', key_name) if not config.lc_config.name: name = utils.ask_str("Server name (note that an 11 character suffix" " will be added to this name): ") config.set_config_option('launch-configuration', 'name', name) if config.get('launch-configuration', 'cloud_init') is None: print("When servers are booted up, the contents of the cloud-init" " script will be executed on the server. This is a way to" " install and configure the software the machine needs" " in order to serve its purpose.\n" "To use the default - input: templates/cloud-init.yml.j2") cloud_init = utils.ask_file("Path to cloud-init script: ") config.set_config_option('launch-configuration', 'cloud_init', cloud_init) if not isinstance(config.lc_config.networks, list): networks = [] utils.print_msg( "Supply one or more networks you wish to" "attach the cloud servers to", bcolors.QUESTION) while True: network = utils.get_object_from_list(pyrax.cloud_networks, "network", quit_option=True) if network and network not in networks: networks.append(str(network)) elif network: pass else: break config.set_config_option('launch-configuration', 'networks', networks) if not config.get('launch-configuration', 'skip_default_networks'): print("By default, the launch config will contain the default" " networks (PublicNet and ServiceNet). You can optionally" " disable these, but some Rackspace services will not function" " properly without them, and they are obligatory on Managed" " service levels") keep = utils.ask_str("Keep default networks? (y/n): ", yesno=True) if keep: config.set_config_option('launch-configuration', 'skip_default_networks', False) else: config.set_config_option('launch-configuration', 'skip_default_networks', True) if not config.get('launch-configuration', 'disk_config'): disk_config = utils.ask_str( "Disk config method (AUTO or MANUAL): ", allowed_input=['AUTO', 'MANUAL', 'auto', 'manual']) config.set_config_option('launch-configuration', 'disk_config', disk_config.upper()) ## # Write [rax-autoscaler] config ## if not isinstance(config.ras_config.load_balancers, list): load_balancers = [] utils.print_msg( "Supply one or more load balancers you wish to" " attach the cloud servers to", bcolors.QUESTION) while True: load_balancer = utils.get_object_from_list( pyrax.cloud_loadbalancers, "load balancer", quit_option=True) if load_balancer and load_balancer not in load_balancers: load_balancers.append(load_balancer) elif load_balancer: pass else: break config.set_config_option('rax-autoscaler', 'load_balancers', load_balancers) if not isinstance(config.ras_config.num_static_servers, int): num_static_servers = utils.ask_integer( "How many nodes in the load balancer are" " not part of the scaling group? (0 for none): ") config.set_config_option('rax-autoscaler', 'num_static_servers', num_static_servers) if not isinstance(config.ras_config.private_key, str) or \ not utils.is_readable(config.ras_config.private_key): print("When scaled up, the servers need to log in to the admin server" " in order to download the playbook, or perform other tasks as" " laid out in the cloud-init template.\nSupply a private key" " which can be used to log in as the user 'autoscale' on the" " admin server") private_key = utils.ask_file("Private key to inject" " into /root/.ssh/id_rsa on servers: ") config.set_config_option('rax-autoscaler', 'private_key', private_key) if not isinstance(config.ras_config.admin_server, str): admin_server = utils.ask_str("IP or host-name of admin server to" " download playbook from: ") config.set_config_option('rax-autoscaler', 'admin_server', admin_server) # Re-parse the file on-disk and validate config.parse_config() config.validate()