From 574163c949e671f179f3a6215834d5936c3a345b Mon Sep 17 00:00:00 2001 From: Francisco Ros Date: Mon, 25 Apr 2016 17:42:57 +0200 Subject: [PATCH] Brook inventory (#15264) * Initial work on Brook.io dynamic inventory * Handle error cases in Brook.io dynamic inventory * Remove defaults from brook.ini * Update Brook.io dynamic inventory for libbrookv0.3 Use authentication api to obtain a valid JWT from an API Token. * Remove defaults from brook.ini --- contrib/inventory/brook.ini | 40 ++++++ contrib/inventory/brook.py | 262 ++++++++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 contrib/inventory/brook.ini create mode 100755 contrib/inventory/brook.py diff --git a/contrib/inventory/brook.ini b/contrib/inventory/brook.ini new file mode 100644 index 00000000000..68f0bb11371 --- /dev/null +++ b/contrib/inventory/brook.ini @@ -0,0 +1,40 @@ +#!/usr/bin/python +# Copyright 2016 Doalitic. +# +# 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 . + +# The Brook.io inventory script has the following dependencies: +# 1. A working Brook.io account +# See https://brook.io +# 2. A valid token generated through the 'API token' panel of Brook.io +# 3. The libbrook python libray. +# See https://github.com/doalitic/libbrook +# +# Author: Francisco Ros + +[brook] +# Valid API token (required). +# E.g. 'Aed342a12A60433697281FeEe1a4037C' +# +api_token = + +# Project id within Brook.io, as obtained from the project settings (optional). If provided, the +# generated inventory will just include the hosts that belong to such project. Otherwise, it will +# include all hosts in projects the requesting user has access to. The response includes groups +# 'project_x', being 'x' the project name. +# E.g. '2e8e099e1bc34cc0979d97ac34e9577b' +# +project_id = diff --git a/contrib/inventory/brook.py b/contrib/inventory/brook.py new file mode 100755 index 00000000000..0de6ffd335e --- /dev/null +++ b/contrib/inventory/brook.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# Copyright 2016 Doalitic. +# +# 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 . + +""" +Brook.io external inventory script +================================== + +Generates inventory that Ansible can understand by making API requests to Brook.io via the libbrook +library. Hence, such dependency must be installed in the system to run this script. + +The default configuration file is named 'brook.ini' and is located alongside this script. You can +choose any other file by setting the BROOK_INI_PATH environment variable. + +If param 'project_id' is left blank in 'brook.ini', the inventory includes all the instances in +projects where the requesting user belongs. Otherwise, only instances from the given project are +included, provided the requesting user belongs to it. + +The following variables are established for every host. They can be retrieved from the hostvars +dictionary. + - brook_name: str + - brook_description: str + - brook_project: str + - brook_template: str + - brook_region: str + - brook_status: str + - brook_tags: list(str) + - brook_internal_ips: list(str) + - brook_external_ips: list(str) + - brook_created_at + - brook_updated_at + - ansible_ssh_host + +Instances are grouped by the following categories: + - tag: + A group is created for each tag. E.g. groups 'tag_foo' and 'tag_bar' are created if there exist + instances with tags 'foo' and/or 'bar'. + - project: + A group is created for each project. E.g. group 'project_test' is created if a project named + 'test' exist. + - status: + A group is created for each instance state. E.g. groups 'status_RUNNING' and 'status_PENDING' + are created if there are instances in running and pending state. + +Examples: + Execute uname on all instances in project 'test' + $ ansible -i brook.py project_test -m shell -a "/bin/uname -a" + + Install nginx on all debian web servers tagged with 'www' + $ ansible -i brook.py tag_www -m apt -a "name=nginx state=present" + + Run site.yml playbook on web servers + $ ansible-playbook -i brook.py site.yml -l tag_www + +Support: + This script is tested on Python 2.7 and 3.4. It may work on other versions though. + +Author: Francisco Ros +Version: 0.1 +""" + + +import sys +import os + +try: + from ConfigParser import SafeConfigParser as ConfigParser +except ImportError: + from configparser import ConfigParser + +try: + import json +except ImportError: + import simplejson as json + +try: + import libbrook +except: + print('Brook.io inventory script requires libbrook. See https://github.com/doalitic/libbrook') + sys.exit(1) + + +class BrookInventory: + + _API_ENDPOINT = 'https://api.brook.io' + + def __init__(self): + self._configure_from_file() + self.client = self.get_api_client() + self.inventory = self.get_inventory() + + def _configure_from_file(self): + """Initialize from .ini file. + + Configuration file is assumed to be named 'brook.ini' and to be located on the same + directory than this file, unless the environment variable BROOK_INI_PATH says otherwise. + """ + + brook_ini_default_path = \ + os.path.join(os.path.dirname(os.path.realpath(__file__)), 'brook.ini') + brook_ini_path = os.environ.get('BROOK_INI_PATH', brook_ini_default_path) + + config = ConfigParser(defaults={ + 'api_token': '', + 'project_id': '' + }) + config.read(brook_ini_path) + self.api_token = config.get('brook', 'api_token') + self.project_id = config.get('brook', 'project_id') + + if not self.api_token: + print('You must provide (at least) your Brook.io API token to generate the dynamic ' + 'inventory.') + sys.exit(1) + + def get_api_client(self): + """Authenticate user via the provided credentials and return the corresponding API client. + """ + + # Get JWT token from API token + # + unauthenticated_client = libbrook.ApiClient(host=self._API_ENDPOINT) + auth_api = libbrook.AuthApi(unauthenticated_client) + api_token = libbrook.AuthTokenRequest() + api_token.token = self.api_token + jwt = auth_api.auth_token(token=api_token) + + # Create authenticated API client + # + return libbrook.ApiClient(host=self._API_ENDPOINT, + header_name='Authorization', + header_value='Bearer %s' % jwt.token) + + def get_inventory(self): + """Generate Ansible inventory. + """ + + groups = dict() + meta = dict() + meta['hostvars'] = dict() + + instances_api = libbrook.InstancesApi(self.client) + projects_api = libbrook.ProjectsApi(self.client) + templates_api = libbrook.TemplatesApi(self.client) + + # If no project is given, get all projects the requesting user has access to + # + if not self.project_id: + projects = [project.id for project in projects_api.index_projects()] + else: + projects = [self.project_id] + + # Build inventory from instances in all projects + # + for project_id in projects: + project = projects_api.show_project(project_id=project_id) + for instance in instances_api.index_instances(project_id=project_id): + # Get template used for this instance + template = templates_api.show_template(template_id=instance.template) + + # Update hostvars + try: + meta['hostvars'][instance.name] = \ + self.hostvars(project, instance, template, instances_api) + except libbrook.rest.ApiException: + continue + + # Group by project + project_group = 'project_%s' % project.name + if project_group in groups.keys(): + groups[project_group].append(instance.name) + else: + groups[project_group] = [instance.name] + + # Group by status + status_group = 'status_%s' % meta['hostvars'][instance.name]['brook_status'] + if status_group in groups.keys(): + groups[status_group].append(instance.name) + else: + groups[status_group] = [instance.name] + + # Group by tags + tags = meta['hostvars'][instance.name]['brook_tags'] + for tag in tags: + tag_group = 'tag_%s' % tag + if tag_group in groups.keys(): + groups[tag_group].append(instance.name) + else: + groups[tag_group] = [instance.name] + + groups['_meta'] = meta + return groups + + def hostvars(self, project, instance, template, api): + """Return the hostvars dictionary for the given instance. + + Raise libbrook.rest.ApiException if it cannot retrieve all required information from the + Brook.io API. + """ + + hostvars = instance.to_dict() + hostvars['brook_name'] = hostvars.pop('name') + hostvars['brook_description'] = hostvars.pop('description') + hostvars['brook_project'] = hostvars.pop('project') + hostvars['brook_template'] = hostvars.pop('template') + hostvars['brook_region'] = hostvars.pop('region') + hostvars['brook_created_at'] = hostvars.pop('created_at') + hostvars['brook_updated_at'] = hostvars.pop('updated_at') + del hostvars['id'] + del hostvars['key'] + del hostvars['provider'] + del hostvars['image'] + + # Substitute identifiers for names + # + hostvars['brook_project'] = project.name + hostvars['brook_template'] = template.name + + # Retrieve instance state + # + status = api.status_instance(project_id=project.id, instance_id=instance.id) + hostvars.update({'brook_status': status.state}) + + # Retrieve instance tags + # + tags = api.instance_tags(project_id=project.id, instance_id=instance.id) + hostvars.update({'brook_tags': tags}) + + # Retrieve instance addresses + # + addresses = api.instance_addresses(project_id=project.id, instance_id=instance.id) + internal_ips = [address.address for address in addresses if address.scope == 'internal'] + external_ips = [address.address for address in addresses + if address.address and address.scope == 'external'] + hostvars.update({'brook_internal_ips': internal_ips}) + hostvars.update({'brook_external_ips': external_ips}) + try: + hostvars.update({'ansible_ssh_host': external_ips[0]}) + except IndexError: + raise libbrook.rest.ApiException(status='502', reason='Instance without public IP') + + return hostvars + + +# Run the script +# +brook = BrookInventory() +print(json.dumps(brook.inventory))