diff options
Diffstat (limited to 'contrib/inventory/packet_net.py')
-rwxr-xr-x | contrib/inventory/packet_net.py | 506 |
1 files changed, 0 insertions, 506 deletions
diff --git a/contrib/inventory/packet_net.py b/contrib/inventory/packet_net.py deleted file mode 100755 index 22f989a9d9..0000000000 --- a/contrib/inventory/packet_net.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env python - -''' -Packet.net external inventory script -================================= - -Generates inventory that Ansible can understand by making API request to -Packet.net using the Packet library. - -NOTE: This script assumes Ansible is being executed where the environment -variable needed for Packet API Token already been set: - export PACKET_API_TOKEN=Bfse9F24SFtfs423Gsd3ifGsd43sSdfs - -This script also assumes there is a packet_net.ini file alongside it. To specify a -different path to packet_net.ini, define the PACKET_NET_INI_PATH environment variable: - - export PACKET_NET_INI_PATH=/path/to/my_packet_net.ini - -''' - -# (c) 2016, Peter Sankauskas -# (c) 2017, Tomas Karasek -# -# This file is part of Ansible, -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -###################################################################### - -import sys -import os -import argparse -import re -from time import time - -from ansible.module_utils import six -from ansible.module_utils.six.moves import configparser - -try: - import packet -except ImportError as e: - sys.exit("failed=True msg='`packet-python` library required for this script'") - -import traceback - - -import json - - -ini_section = 'packet' - - -class PacketInventory(object): - - def _empty_inventory(self): - return {"_meta": {"hostvars": {}}} - - def __init__(self): - ''' Main execution path ''' - - # Inventory grouped by device IDs, tags, security groups, regions, - # and availability zones - self.inventory = self._empty_inventory() - - # Index of hostname (address) to device ID - self.index = {} - - # Read settings and parse CLI arguments - self.parse_cli_args() - self.read_settings() - - # Cache - if self.args.refresh_cache: - self.do_api_calls_update_cache() - elif not self.is_cache_valid(): - self.do_api_calls_update_cache() - - # Data to print - if self.args.host: - data_to_print = self.get_host_info() - - elif self.args.list: - # Display list of devices for inventory - if self.inventory == self._empty_inventory(): - data_to_print = self.get_inventory_from_cache() - else: - data_to_print = self.json_format_dict(self.inventory, True) - - print(data_to_print) - - 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_cache): - mod_time = os.path.getmtime(self.cache_path_cache) - current_time = time() - if (mod_time + self.cache_max_age) > current_time: - if os.path.isfile(self.cache_path_index): - return True - - return False - - def read_settings(self): - ''' Reads the settings from the packet_net.ini file ''' - if six.PY3: - config = configparser.ConfigParser() - else: - config = configparser.SafeConfigParser() - - _ini_path_raw = os.environ.get('PACKET_NET_INI_PATH') - - if _ini_path_raw: - packet_ini_path = os.path.expanduser(os.path.expandvars(_ini_path_raw)) - else: - packet_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'packet_net.ini') - config.read(packet_ini_path) - - # items per page - self.items_per_page = 999 - if config.has_option(ini_section, 'items_per_page'): - config.get(ini_section, 'items_per_page') - - # Instance states to be gathered in inventory. Default is all of them. - packet_valid_device_states = [ - 'active', - 'inactive', - 'queued', - 'provisioning' - ] - self.packet_device_states = [] - if config.has_option(ini_section, 'device_states'): - for device_state in config.get(ini_section, 'device_states').split(','): - device_state = device_state.strip() - if device_state not in packet_valid_device_states: - continue - self.packet_device_states.append(device_state) - else: - self.packet_device_states = packet_valid_device_states - - # Cache related - cache_dir = os.path.expanduser(config.get(ini_section, 'cache_path')) - if not os.path.exists(cache_dir): - os.makedirs(cache_dir) - - self.cache_path_cache = cache_dir + "/ansible-packet.cache" - self.cache_path_index = cache_dir + "/ansible-packet.index" - self.cache_max_age = config.getint(ini_section, 'cache_max_age') - - # Configure nested groups instead of flat namespace. - if config.has_option(ini_section, 'nested_groups'): - self.nested_groups = config.getboolean(ini_section, 'nested_groups') - else: - self.nested_groups = False - - # Replace dash or not in group names - if config.has_option(ini_section, 'replace_dash_in_groups'): - self.replace_dash_in_groups = config.getboolean(ini_section, 'replace_dash_in_groups') - else: - self.replace_dash_in_groups = True - - # Configure which groups should be created. - group_by_options = [ - 'group_by_device_id', - 'group_by_hostname', - 'group_by_facility', - 'group_by_project', - 'group_by_operating_system', - 'group_by_plan_type', - 'group_by_tags', - 'group_by_tag_none', - ] - for option in group_by_options: - if config.has_option(ini_section, option): - setattr(self, option, config.getboolean(ini_section, option)) - else: - setattr(self, option, True) - - # Do we need to just include hosts that match a pattern? - try: - pattern_include = config.get(ini_section, 'pattern_include') - if pattern_include and len(pattern_include) > 0: - self.pattern_include = re.compile(pattern_include) - else: - self.pattern_include = None - except configparser.NoOptionError: - self.pattern_include = None - - # Do we need to exclude hosts that match a pattern? - try: - pattern_exclude = config.get(ini_section, 'pattern_exclude') - if pattern_exclude and len(pattern_exclude) > 0: - self.pattern_exclude = re.compile(pattern_exclude) - else: - self.pattern_exclude = None - except configparser.NoOptionError: - self.pattern_exclude = None - - # Projects - self.projects = [] - configProjects = config.get(ini_section, 'projects') - configProjects_exclude = config.get(ini_section, 'projects_exclude') - if (configProjects == 'all'): - for projectInfo in self.get_projects(): - if projectInfo.name not in configProjects_exclude: - self.projects.append(projectInfo.name) - else: - self.projects = configProjects.split(",") - - def parse_cli_args(self): - ''' Command line argument processing ''' - - parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Packet') - parser.add_argument('--list', action='store_true', default=True, - help='List Devices (default: True)') - parser.add_argument('--host', action='store', - help='Get all the variables about a specific device') - parser.add_argument('--refresh-cache', action='store_true', default=False, - help='Force refresh of cache by making API requests to Packet (default: False - use cache files)') - self.args = parser.parse_args() - - def do_api_calls_update_cache(self): - ''' Do API calls to each region, and save data in cache files ''' - - for projectInfo in self.get_projects(): - if projectInfo.name in self.projects: - self.get_devices_by_project(projectInfo) - - self.write_to_cache(self.inventory, self.cache_path_cache) - self.write_to_cache(self.index, self.cache_path_index) - - def connect(self): - ''' create connection to api server''' - token = os.environ.get('PACKET_API_TOKEN') - if token is None: - raise Exception("Error reading token from environment (PACKET_API_TOKEN)!") - manager = packet.Manager(auth_token=token) - return manager - - def get_projects(self): - '''Makes a Packet API call to get the list of projects''' - - params = { - 'per_page': self.items_per_page - } - - try: - manager = self.connect() - projects = manager.list_projects(params=params) - return projects - except Exception as e: - traceback.print_exc() - self.fail_with_error(e, 'getting Packet projects') - - def get_devices_by_project(self, project): - ''' Makes an Packet API call to the list of devices in a particular - project ''' - - params = { - 'per_page': self.items_per_page - } - - try: - manager = self.connect() - devices = manager.list_devices(project_id=project.id, params=params) - - for device in devices: - self.add_device(device, project) - - except Exception as e: - traceback.print_exc() - self.fail_with_error(e, 'getting Packet devices') - - def fail_with_error(self, err_msg, err_operation=None): - '''log an error to std err for ansible-playbook to consume and exit''' - if err_operation: - err_msg = 'ERROR: "{err_msg}", while: {err_operation}\n'.format( - err_msg=err_msg, err_operation=err_operation) - sys.stderr.write(err_msg) - sys.exit(1) - - def get_device(self, device_id): - manager = self.connect() - - device = manager.get_device(device_id) - return device - - def add_device(self, device, project): - ''' Adds a device to the inventory and index, as long as it is - addressable ''' - - # Only return devices with desired device states - if device.state not in self.packet_device_states: - return - - # Select the best destination address. Only include management - # addresses as non-management (elastic) addresses need manual - # host configuration to be routable. - # See https://help.packet.net/article/54-elastic-ips. - dest = None - for ip_address in device.ip_addresses: - if ip_address['public'] is True and \ - ip_address['address_family'] == 4 and \ - ip_address['management'] is True: - dest = ip_address['address'] - - if not dest: - # Skip devices we cannot address (e.g. private VPC subnet) - return - - # if we only want to include hosts that match a pattern, skip those that don't - if self.pattern_include and not self.pattern_include.match(device.hostname): - return - - # if we need to exclude hosts that match a pattern, skip those - if self.pattern_exclude and self.pattern_exclude.match(device.hostname): - return - - # Add to index - self.index[dest] = [project.id, device.id] - - # Inventory: Group by device ID (always a group of 1) - if self.group_by_device_id: - self.inventory[device.id] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'devices', device.id) - - # Inventory: Group by device name (hopefully a group of 1) - if self.group_by_hostname: - self.push(self.inventory, device.hostname, dest) - if self.nested_groups: - self.push_group(self.inventory, 'hostnames', project.name) - - # Inventory: Group by project - if self.group_by_project: - self.push(self.inventory, project.name, dest) - if self.nested_groups: - self.push_group(self.inventory, 'projects', project.name) - - # Inventory: Group by facility - if self.group_by_facility: - self.push(self.inventory, device.facility['code'], dest) - if self.nested_groups: - if self.group_by_facility: - self.push_group(self.inventory, project.name, device.facility['code']) - - # Inventory: Group by OS - if self.group_by_operating_system: - self.push(self.inventory, device.operating_system['slug'], dest) - if self.nested_groups: - self.push_group(self.inventory, 'operating_systems', device.operating_system['slug']) - - # Inventory: Group by plan type - if self.group_by_plan_type: - self.push(self.inventory, device.plan['slug'], dest) - if self.nested_groups: - self.push_group(self.inventory, 'plans', device.plan['slug']) - - # Inventory: Group by tag keys - if self.group_by_tags: - for k in device.tags: - key = self.to_safe("tag_" + k) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) - - # Global Tag: devices without tags - if self.group_by_tag_none and len(device.tags) == 0: - self.push(self.inventory, 'tag_none', dest) - if self.nested_groups: - self.push_group(self.inventory, 'tags', 'tag_none') - - # Global Tag: tag all Packet devices - self.push(self.inventory, 'packet', dest) - - self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_device(device) - - def get_host_info_dict_from_device(self, device): - device_vars = {} - for key in vars(device): - value = getattr(device, key) - key = self.to_safe('packet_' + key) - - # Handle complex types - if key == 'packet_state': - device_vars[key] = device.state or '' - elif key == 'packet_hostname': - device_vars[key] = value - elif isinstance(value, (int, bool)): - device_vars[key] = value - elif isinstance(value, six.string_types): - device_vars[key] = value.strip() - elif value is None: - device_vars[key] = '' - elif key == 'packet_facility': - device_vars[key] = value['code'] - elif key == 'packet_operating_system': - device_vars[key] = value['slug'] - elif key == 'packet_plan': - device_vars[key] = value['slug'] - elif key == 'packet_tags': - for k in value: - key = self.to_safe('packet_tag_' + k) - device_vars[key] = k - else: - pass - # print key - # print type(value) - # print value - - return device_vars - - def get_host_info(self): - ''' Get variables about a specific host ''' - - if len(self.index) == 0: - # Need to load index from cache - self.load_index_from_cache() - - if self.args.host not in self.index: - # try updating the cache - self.do_api_calls_update_cache() - if self.args.host not in self.index: - # host might not exist anymore - return self.json_format_dict({}, True) - - (project_id, device_id) = self.index[self.args.host] - - device = self.get_device(device_id) - return self.json_format_dict(self.get_host_info_dict_from_device(device), True) - - def push(self, my_dict, key, element): - ''' Push an element onto an array that may not have been defined in - the dict ''' - group_info = my_dict.setdefault(key, []) - if isinstance(group_info, dict): - host_list = group_info.setdefault('hosts', []) - host_list.append(element) - else: - group_info.append(element) - - def push_group(self, my_dict, key, element): - ''' Push a group as a child of another group. ''' - parent_group = my_dict.setdefault(key, {}) - if not isinstance(parent_group, dict): - parent_group = my_dict[key] = {'hosts': parent_group} - child_groups = parent_group.setdefault('children', []) - if element not in child_groups: - child_groups.append(element) - - def get_inventory_from_cache(self): - ''' Reads the inventory from the cache file and returns it as a JSON - object ''' - - cache = open(self.cache_path_cache, 'r') - json_inventory = cache.read() - return json_inventory - - def load_index_from_cache(self): - ''' Reads the index from the cache file sets self.index ''' - - cache = open(self.cache_path_index, 'r') - json_index = cache.read() - self.index = json.loads(json_index) - - def write_to_cache(self, data, filename): - ''' Writes data in JSON format to a file ''' - - json_data = self.json_format_dict(data, True) - cache = open(filename, 'w') - cache.write(json_data) - cache.close() - - def uncammelize(self, key): - temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower() - - def to_safe(self, word): - ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' - regex = r"[^A-Za-z0-9\_" - if not self.replace_dash_in_groups: - regex += r"\-" - return re.sub(regex + "]", "_", word) - - def json_format_dict(self, 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) - - -# Run the script -PacketInventory() |