Пример #1
0
    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)
Пример #2
0
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)
Пример #3
0
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
Пример #4
0
 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()
Пример #5
0
 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
Пример #6
0
 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)
Пример #7
0
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)
Пример #8
0
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
Пример #9
0
    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
Пример #10
0
 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
Пример #11
0
    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)
Пример #12
0
    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
Пример #13
0
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)
Пример #14
0
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()