diff options
Diffstat (limited to 'inventory')
| -rwxr-xr-x | inventory/multi_inventory.py | 462 | ||||
| -rw-r--r-- | inventory/multi_inventory.yaml.example | 51 | 
2 files changed, 0 insertions, 513 deletions
diff --git a/inventory/multi_inventory.py b/inventory/multi_inventory.py deleted file mode 100755 index be597267e..000000000 --- a/inventory/multi_inventory.py +++ /dev/null @@ -1,462 +0,0 @@ -#!/usr/bin/env python2 -''' -    Fetch and combine multiple inventory account settings into a single -    json hash. -''' -# vim: expandtab:tabstop=4:shiftwidth=4 - -from time import time -import argparse -import yaml -import os -import subprocess -import json -import errno -import fcntl -import tempfile -import copy -from string import Template -import shutil - -CONFIG_FILE_NAME = 'multi_inventory.yaml' -DEFAULT_CACHE_PATH = os.path.expanduser('~/.ansible/tmp/multi_inventory.cache') - -class MultiInventoryException(Exception): -    '''Exceptions for MultiInventory class''' -    pass - -# pylint: disable=too-many-public-methods -# After a refactor of too-many-branches and placing those branches into -# their own corresponding function, we have passed the allowed amount of functions(20). -class MultiInventory(object): -    ''' -       MultiInventory class: -            Opens a yaml config file and reads aws credentials. -            Stores a json hash of resources in result. -    ''' - -    def __init__(self, args=None): -        # Allow args to be passed when called as a library -        if not args: -            self.args = {} -        else: -            self.args = args - -        self.cache_path = DEFAULT_CACHE_PATH -        self.config = None -        self.all_inventory_results = {} -        self.result = {} -        self.file_path = os.path.join(os.path.dirname(os.path.realpath(__file__))) - -        same_dir_config_file = os.path.join(self.file_path, CONFIG_FILE_NAME) -        etc_dir_config_file = os.path.join(os.path.sep, 'etc', 'ansible', CONFIG_FILE_NAME) - -        # Prefer a file in the same directory, fall back to a file in etc -        if os.path.isfile(same_dir_config_file): -            self.config_file = same_dir_config_file -        elif os.path.isfile(etc_dir_config_file): -            self.config_file = etc_dir_config_file -        else: -            self.config_file = None # expect env vars - -        # load yaml -        if self.config_file and os.path.isfile(self.config_file): -            self.config = self.load_yaml_config() -        elif os.environ.has_key("AWS_ACCESS_KEY_ID") and \ -             os.environ.has_key("AWS_SECRET_ACCESS_KEY"): -            # Build a default config -            self.config = {} -            self.config['accounts'] = [ -                { -                    'name': 'default', -                    'cache_location': DEFAULT_CACHE_PATH, -                    'provider': 'aws/hosts/ec2.py', -                    'env_vars': { -                        'AWS_ACCESS_KEY_ID':     os.environ["AWS_ACCESS_KEY_ID"], -                        'AWS_SECRET_ACCESS_KEY': os.environ["AWS_SECRET_ACCESS_KEY"], -                    } -                }, -            ] - -            self.config['cache_max_age'] = 300 -        else: -            raise RuntimeError("Could not find valid ec2 credentials in the environment.") - -        if self.config.has_key('cache_location'): -            self.cache_path = self.config['cache_location'] - -    def run(self): -        '''This method checks to see if the local -           cache is valid for the inventory. - -           if the cache is valid; return cache -           else the credentials are loaded from multi_inventory.yaml or from the env -           and we attempt to get the inventory from the provider specified. -        ''' - -        if self.args.get('refresh_cache', None): -            self.get_inventory() -            self.write_to_cache() -        # if its a host query, fetch and do not cache -        elif self.args.get('host', None): -            self.get_inventory() -        elif not self.is_cache_valid(): -            # go fetch the inventories and cache them if cache is expired -            self.get_inventory() -            self.write_to_cache() -        else: -            # get data from disk -            self.get_inventory_from_cache() - -    def load_yaml_config(self, conf_file=None): -        """Load a yaml config file with credentials to query the -        respective cloud for inventory. -        """ -        config = None - -        if not conf_file: -            conf_file = self.config_file - -        with open(conf_file) as conf: -            config = yaml.safe_load(conf) - -        # Provide a check for unique account names -        if len(set([acc['name'] for acc in config['accounts']])) != len(config['accounts']): -            raise MultiInventoryException('Duplicate account names in config file') - -        return config - -    def get_provider_tags(self, provider, env=None): -        """Call <provider> and query all of the tags that are usuable -        by ansible.  If environment is empty use the default env. -        """ -        if not env: -            env = os.environ - -        # Allow relatively path'd providers in config file -        if os.path.isfile(os.path.join(self.file_path, provider)): -            provider = os.path.join(self.file_path, provider) - -        # check to see if provider exists -        if not os.path.isfile(provider) or not os.access(provider, os.X_OK): -            raise RuntimeError("Problem with the provider.  Please check path " \ -                        "and that it is executable. (%s)" % provider) - -        cmds = [provider] -        if self.args.get('host', None): -            cmds.append("--host") -            cmds.append(self.args.get('host', None)) -        else: -            cmds.append('--list') - -        if 'aws' in provider.lower(): -            cmds.append('--refresh-cache') - -        return subprocess.Popen(cmds, stderr=subprocess.PIPE, \ -                                stdout=subprocess.PIPE, env=env) - -    @staticmethod -    def generate_config(provider_files): -        """Generate the provider_files in a temporary directory. -        """ -        prefix = 'multi_inventory.' -        tmp_dir_path = tempfile.mkdtemp(prefix=prefix) -        for provider_file in provider_files: -            filedes = open(os.path.join(tmp_dir_path, provider_file['name']), 'w+') -            content = Template(provider_file['contents']).substitute(tmpdir=tmp_dir_path) -            filedes.write(content) -            filedes.close() - -        return tmp_dir_path - -    def run_provider(self): -        '''Setup the provider call with proper variables -           and call self.get_provider_tags. -        ''' -        try: -            all_results = [] -            tmp_dir_paths = [] -            processes = {} -            for account in self.config['accounts']: -                tmp_dir = None -                if account.has_key('provider_files'): -                    tmp_dir = MultiInventory.generate_config(account['provider_files']) -                    tmp_dir_paths.append(tmp_dir) - -                # Update env vars after creating provider_config_files -                # so that we can grab the tmp_dir if it exists -                env = account.get('env_vars', {}) -                if env and tmp_dir: -                    for key, value in env.items(): -                        env[key] = Template(value).substitute(tmpdir=tmp_dir) - -                name = account['name'] -                provider = account['provider'] -                processes[name] = self.get_provider_tags(provider, env) - -            # for each process collect stdout when its available -            for name, process in processes.items(): -                out, err = process.communicate() -                all_results.append({ -                    "name": name, -                    "out": out.strip(), -                    "err": err.strip(), -                    "code": process.returncode -                }) - -        finally: -            # Clean up the mkdtemp dirs -            for tmp_dir in tmp_dir_paths: -                shutil.rmtree(tmp_dir) - -        return all_results - -    def get_inventory(self): -        """Create the subprocess to fetch tags from a provider. -        Host query: -        Query to return a specific host.  If > 1 queries have -        results then fail. - -        List query: -        Query all of the different accounts for their tags.  Once completed -        store all of their results into one merged updated hash. -        """ -        provider_results = self.run_provider() - -        # process --host results -        # For any 0 result, return it -        if self.args.get('host', None): -            count = 0 -            for results in provider_results: -                if results['code'] == 0 and results['err'] == '' and results['out'] != '{}': -                    self.result = json.loads(results['out']) -                    count += 1 -                if count > 1: -                    raise RuntimeError("Found > 1 results for --host %s. \ -                                       This is an invalid state." % self.args.get('host', None)) -        # process --list results -        else: -            # For any non-zero, raise an error on it -            for result in provider_results: -                if result['code'] != 0: -                    err_msg = ['\nProblem fetching account: {name}', -                               'Error Code: {code}', -                               'StdErr: {err}', -                               'Stdout: {out}', -                              ] -                    raise RuntimeError('\n'.join(err_msg).format(**result)) -                else: -                    self.all_inventory_results[result['name']] = json.loads(result['out']) - -            # Check if user wants extra vars in yaml by -            # having hostvars and all_group defined -            for acc_config in self.config['accounts']: -                self.apply_account_config(acc_config) - -            # Build results by merging all dictionaries -            values = self.all_inventory_results.values() -            values.insert(0, self.result) -            for result in  values: -                MultiInventory.merge_destructively(self.result, result) - -    def add_entry(self, data, keys, item): -        ''' Add an item to a dictionary with key notation a.b.c -            d = {'a': {'b': 'c'}}} -            keys = a.b -            item = c -        ''' -        if "." in keys: -            key, rest = keys.split(".", 1) -            if key not in data: -                data[key] = {} -            self.add_entry(data[key], rest, item) -        else: -            data[keys] = item - -    def get_entry(self, data, keys): -        ''' Get an item from a dictionary with key notation a.b.c -            d = {'a': {'b': 'c'}}} -            keys = a.b -            return c -        ''' -        if keys and "." in keys: -            key, rest = keys.split(".", 1) -            return self.get_entry(data[key], rest) -        else: -            return data.get(keys, None) - -    def apply_extra_vars(self, inventory, extra_vars): -        ''' Apply the account config extra vars ''' -        # Extra vars go here -        for new_var, value in extra_vars.items(): -            for data in inventory.values(): -                self.add_entry(data, new_var, value) - -    def apply_clone_vars(self, inventory, clone_vars): -        ''' Apply the account config clone vars ''' -        # Clone vars go here -        for to_name, from_name in clone_vars.items(): -            for data in inventory.values(): -                self.add_entry(data, to_name, self.get_entry(data, from_name)) - -    def apply_extra_groups(self, inventory, extra_groups): -        ''' Apply the account config for extra groups ''' -        _ = self # Here for pylint.  wanted an instance method instead of static -        for new_var, value in extra_groups.items(): -            for _ in inventory['_meta']['hostvars'].values(): -                inventory["%s_%s" % (new_var, value)] = copy.copy(inventory['all_hosts']) - -    def apply_clone_groups(self, inventory, clone_groups): -        ''' Apply the account config for clone groups ''' -        for to_name, from_name in clone_groups.items(): -            for name, data in inventory['_meta']['hostvars'].items(): -                key = '%s_%s' % (to_name, self.get_entry(data, from_name)) -                if not inventory.has_key(key): -                    inventory[key] = [] -                inventory[key].append(name) - -    def apply_group_selectors(self, inventory, group_selectors): -        ''' Apply the account config for group selectors ''' -        _ = self # Here for pylint.  wanted an instance method instead of static -        # There could be multiple clusters per account.  We need to process these selectors -        # based upon the oo_clusterid_ variable. -        clusterids = [group for group in inventory if "oo_clusterid_" in group] - -        for clusterid in clusterids: -            for selector in group_selectors: -                if inventory.has_key(selector['from_group']): -                    hosts = list(set(inventory[clusterid]) & set(inventory[selector['from_group']])) -                    hosts.sort() - -                    # Multiple clusters in an account -                    if inventory.has_key(selector['name']): -                        inventory[selector['name']].extend(hosts[0:selector['count']]) -                    else: -                        inventory[selector['name']] = hosts[0:selector['count']] - -                    for host in hosts: -                        if host in inventory[selector['name']]: -                            inventory['_meta']['hostvars'][host][selector['name']] = True -                        else: -                            inventory['_meta']['hostvars'][host][selector['name']] = False - -    def apply_account_config(self, acc_config): -        ''' Apply account config settings ''' -        results = self.all_inventory_results[acc_config['name']] -        results['all_hosts'] = results['_meta']['hostvars'].keys() - -        self.apply_extra_vars(results['_meta']['hostvars'], acc_config.get('extra_vars', {})) - -        self.apply_clone_vars(results['_meta']['hostvars'], acc_config.get('clone_vars', {})) - -        self.apply_extra_groups(results, acc_config.get('extra_groups', {})) - -        self.apply_clone_groups(results, acc_config.get('clone_groups', {})) - -        self.apply_group_selectors(results, acc_config.get('group_selectors', {})) - -        # store the results back into all_inventory_results -        self.all_inventory_results[acc_config['name']] = results - -    @staticmethod -    def merge_destructively(input_a, input_b): -        "merges b into input_a" -        for key in input_b: -            if key in input_a: -                if isinstance(input_a[key], dict) and isinstance(input_b[key], dict): -                    MultiInventory.merge_destructively(input_a[key], input_b[key]) -                elif input_a[key] == input_b[key]: -                    pass # same leaf value -                # both lists so add each element in b to a if it does ! exist -                elif isinstance(input_a[key], list) and isinstance(input_b[key], list): -                    for result in input_b[key]: -                        if result not in input_a[key]: -                            input_a[key].append(result) -                # a is a list and not b -                elif isinstance(input_a[key], list): -                    if input_b[key] not in input_a[key]: -                        input_a[key].append(input_b[key]) -                elif isinstance(input_b[key], list): -                    input_a[key] = [input_a[key]] + [k for k in input_b[key] if k != input_a[key]] -                else: -                    input_a[key] = [input_a[key], input_b[key]] -            else: -                input_a[key] = input_b[key] -        return input_a - -    def is_cache_valid(self): -        ''' Determines if the cache files have expired, or if it is still valid ''' - -        if os.path.isfile(self.cache_path): -            mod_time = os.path.getmtime(self.cache_path) -            current_time = time() -            if (mod_time + self.config['cache_max_age']) > current_time: -                return True - -        return False - -    def parse_cli_args(self): -        ''' Command line argument processing ''' - -        parser = argparse.ArgumentParser( -            description='Produce an Ansible Inventory file based on a provider') -        parser.add_argument('--refresh-cache', action='store_true', default=False, -                            help='Fetch cached only instances (default: False)') -        parser.add_argument('--list', action='store_true', default=True, -                            help='List instances (default: True)') -        parser.add_argument('--host', action='store', default=False, -                            help='Get all the variables about a specific instance') -        self.args = parser.parse_args().__dict__ - -    def write_to_cache(self): -        ''' Writes data in JSON format to a file ''' - -        # if it does not exist, try and create it. -        if not os.path.isfile(self.cache_path): -            path = os.path.dirname(self.cache_path) -            try: -                os.makedirs(path) -            except OSError as exc: -                if exc.errno != errno.EEXIST or not os.path.isdir(path): -                    raise - -        json_data = MultiInventory.json_format_dict(self.result, True) -        with open(self.cache_path, 'w') as cache: -            try: -                fcntl.flock(cache, fcntl.LOCK_EX) -                cache.write(json_data) -            finally: -                fcntl.flock(cache, fcntl.LOCK_UN) - -    def get_inventory_from_cache(self): -        ''' Reads the inventory from the cache file and returns it as a JSON -        object ''' - -        if not os.path.isfile(self.cache_path): -            return None - -        with open(self.cache_path, 'r') as cache: -            self.result = json.loads(cache.read()) - -        return True - -    @classmethod -    def json_format_dict(cls, data, pretty=False): -        ''' Converts a dict to a JSON object and dumps it as a formatted -        string ''' - -        if pretty: -            return json.dumps(data, sort_keys=True, indent=2) -        else: -            return json.dumps(data) - -    def result_str(self): -        '''Return cache string stored in self.result''' -        return self.json_format_dict(self.result, True) - - -if __name__ == "__main__": -    MI2 = MultiInventory() -    MI2.parse_cli_args() -    MI2.run() -    print MI2.result_str() diff --git a/inventory/multi_inventory.yaml.example b/inventory/multi_inventory.yaml.example deleted file mode 100644 index 0f0788d18..000000000 --- a/inventory/multi_inventory.yaml.example +++ /dev/null @@ -1,51 +0,0 @@ -# multi ec2 inventory configs -# -cache_location: ~/.ansible/tmp/multi_inventory.cache - -accounts: -  - name: aws1 -    provider: aws/ec2.py -    provider_files: -    - name: ec2.ini -      content: |- -        [ec2] -        regions = all -        regions_exclude =  us-gov-west-1,cn-north-1 -        destination_variable = public_dns_name -        route53 = False -        cache_path = ~/.ansible/tmp -        cache_max_age = 300 -        vpc_destination_variable = ip_address -    env_vars: -      AWS_ACCESS_KEY_ID: XXXXXXXXXXXXXXXXXXXX -      AWS_SECRET_ACCESS_KEY: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -      EC2_INI_PATH: ${tmpdir}/ec2.ini # we replace ${tmpdir} with the temporary directory that we've created for the provider. -    extra_vars: -      cloud: aws -      account: aws1 - --   name: mygce -    extra_vars: -      cloud: gce -      account: gce1 -    env_vars: -      GCE_INI_PATH: ${tmpdir}/gce.ini # we replace ${tmpdir} with the temporary directory that we've created for the provider. -    provider: gce/gce.py -    provider_files: -    - name: priv_key.pem -      contents: |- -        -----BEGIN PRIVATE KEY----- -        yourprivatekeydatahere -        -----END PRIVATE KEY----- -    - name: gce.ini -      contents: |- -        [gce] -        gce_service_account_email_address = <uuid>@developer.gserviceaccount.com -        gce_service_account_pem_file_path = ${tmpdir}/priv_key.pem # we replace ${tmpdir} with the temporary directory that we've created for the provider. -        gce_project_id = gce-project -        zone = us-central1-a -        network = default -        gce_machine_type = n1-standard-2 -        gce_machine_image = rhel7 - -cache_max_age: 600  | 
