#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (c) 2020 Oneo GmbH (ConfigSeeder, https://configseeder.com/) and others. # Any form of reproduction, distribution, exploitation or alteration is prohibited without the prior written consent of Oneo GmbH. # from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' author: - Christian Cavegn (@oneo.ch) lookup: configseeder short_description: ConfigSeeder integration for Ansible description: - Retrieves Configuration Data from ConfigSeeder options: apiKey: description: API Key for accessing the config server env: - name: CONFIGSEEDER_APIKEY ini: - section: configseeder key: apiKey required: True type: string url: description: configseeder url env: - name: CONFIGSEEDER_URL ini: - section: configseeder key: url required: True type: string timeout: description: timeout in seconds for accessing configseeder default: 2 env: - name: CONFIGSEEDER_TIMEOUT ini: - section: configseeder key: timeout required: False type: float tenantKey: description: Name of the tenantKey default: default env: - name: CONFIGSEEDER_TENANTKEY ini: - section: configseeder key: tenantKey required: False type: string environmentKey: description: Name of the environmentKey env: - name: CONFIGSEEDER_ENVIRONMENTKEY ini: - section: configseeder key: environmentKey required: True type: string configurationGroupKeys: description: Name of the configuration group keys env: - name: CONFIGSEEDER_CONFIGURATIONGROUPKEYS ini: - section: configseeder key: configurationGroupKeys required: True type: list context: description: Filter Context env: - name: CONFIGSEEDER_CONTEXT ini: - section: configseeder key: context required: False type: string dateTime: description: Filter DateTime env: - name: CONFIGSEEDER_DATETIME ini: - section: configseeder key: dateTime required: False type: string version: description: Filter Version of the application env: - name: CONFIGSEEDER_VERSION ini: - section: configseeder key: version required: False type: string ''' EXAMPLES = ''' - name: "a value from a locally running etcd" debug: msg={{ lookup('configseeder', 'foo/bar') }} - name: "values from multiple folders on a locally running etcd" debug: msg={{ lookup('configseeder', 'foo', 'bar', 'baz') }} - name: "since Ansible 2.5 you can set server options inline" debug: msg="{{ lookup('configseeder', 'foo', version='v2', url='https://staging-postgres-config-seeder.oneo.cloud/') }}" ''' RETURN = ''' _raw: description: - list of values associated with input keys type: list elements: strings ''' import datetime import hashlib import json import platform import re import requests from ansible.plugins.lookup import LookupBase from ansible.utils.display import Display from ansible.errors import AnsibleLookupError from ansible.errors import AnsibleOptionsError from requests.exceptions import ConnectionError CONFIGSEEDER_PLUGIN_VERSION='0.8.0' DATE_TIME_PATTERN='[\d]{4}-[\d]{2}-[\d]{2}[tT][\d]{2}:[\d]{2}:[\d]{2}([zZ]|([\+|\-]([\d]{2}:[\d]{2})))' class LookupModule(LookupBase): def __init__(self, loader=None, templar=None, **kwargs): super(LookupModule, self).__init__(loader, templar, **kwargs) self.display = Display(verbosity=self._display.verbosity) self.configseeder = ConfigSeeder(self.display) def run(self, terms, variables, **kwargs): self.display.v('ConfigSeeder Lookup-Plugin called (Version: {})'.format(CONFIGSEEDER_PLUGIN_VERSION)) self.set_options(var_options=variables, direct=kwargs) configuration = self.get_configseeder_configuration() self.debug_configuration(configuration, kwargs, terms) allData = self.configseeder.loadFromConfigSeeder(configuration) filteredData = self.filter_data(allData, terms) if len(filteredData) == 0: self.display.error('Variable not found for request {}'.format(configuration.__repr__())) return {} if len(terms) == 1: self.display.v('return one value') return [filteredData[terms[0]]] self.display.v('return {} values'.format(len(filteredData))) return filteredData def debug_configuration(self, configuration, kwargs, terms): self.display.vvv('Plugin configuration') self.display.vvv('\tParameters:') self.display.vvv('\t\tTerms: {}'.format(terms)) self.display.vvv('\t\tKwargs: {}'.format(kwargs)) self.display.vvv('') self.display.vvv('\tAccess:') self.display.vvv('\t\tapiKey: ') self.display.vvv('\t\tUrl: {}'.format(configuration.url)) self.display.vvv('\t\tTimeout: {}'.format(configuration.timeout)) self.display.vvv('\t\tEnvironmentKey: {}'.format(configuration.environmentKey)) self.display.vvv('\t\tConfigGroupKeys: {}'.format(configuration.configurationGroupKeys)) self.display.vvv('\t\tContext: {}'.format(configuration.context)) self.display.vvv('\t\tVersion: {}'.format(configuration.version)) self.display.vvv('\t\tDateTime: {}'.format(configuration.dateTime)) self.display.vvv('') def filter_data(self, data, terms): return {k:v for k,v in data.items() if len(terms) == 0 or k in terms} def get_configseeder_configuration(self): # Mandatory parameters apiKey = self.get_option('apiKey') url = self.get_option('url') timeout = self.get_option('timeout') environmentKey = self.get_option('environmentKey') configurationGroupKeys = self.get_option('configurationGroupKeys') # Mandatory parameters with default values tenantKey = self.get_option('tenantKey') if tenantKey is None or len(tenantKey) == 0: tenantKey = 'default' # Optional parameters context = self.get_option('context') version = self.get_option('version') dateTime = self.get_option('dateTime') if context is None or len(context) == 0: self.display.vv("ConfigSeeder option 'context' not set. If required, set in ansible.cfg [configseeder] with key 'context' or with environment variable CONFIGSEEDER_CONTEXT") if version is None or len(version) == 0: self.display.vv("ConfigSeeder option 'version' not set. If required, set in ansible.cfg [configseeder] with key 'version' or with environment variable CONFIGSEEDER_VERSION") if dateTime is None or len(dateTime) == 0: self.display.vv("ConfigSeeder option 'dateTime' not set. If required, set in ansible.cfg [configseeder] with key 'dateTime' or with environment variable CONFIGSEEDER_DATETIME") else: dateTimePattern = re.compile(DATE_TIME_PATTERN) if not dateTimePattern.match(dateTime): raise AnsibleOptionsError("Mandatory ConfigSeeder option 'dateTime ({})' is invalid (doesn't match pattern '2006-01-02T15:04:05Z07:00')".format(dateTime)) return ConfigSeederConfiguration(apiKey, url, timeout, tenantKey, environmentKey, configurationGroupKeys, context, version, dateTime=dateTime) class ConfigSeeder: def __init__(self, display): self.display = display def loadFromConfigSeeder(self, configuration): self.display.v('Loading data from ConfigSeeder...') if configuration.configurationGroupKeys is None or len(configuration.configurationGroupKeys) == 0: return {} headers = self.createHeaders(configuration) request = self.createRequest(configuration) requestJson = json.dumps(request) self.display.vvv('Request: {}'.format(requestJson)) try: response = requests.post(url=configuration.url + '/public/api/v1/configurations', headers=headers, data=requestJson, timeout=configuration.timeout) self.display.vvv('Response: Code: {}, Message: '.format(response.status_code, response.content)) except requests.exceptions.ConnectionError as err: self.display.v('Received error while accessing ConfigSeeder: {}'.format(err)) raise AnsibleLookupError("Unable to connect to ConfigSeeder: {}".format(err.args[0].reason)) if response.status_code != 200: contentType = response.headers['Content-Type'] self.display.v("Content Type: '{}'".format(contentType)) if contentType.index("problem+json") > 0: bodyAsJson = response.json() self.display.v("Received status '{}' and error from ConfigSeeder: {}".format(response.status_code, bodyAsJson)) raise AnsibleLookupError("Received status '{}' from ConfigSeeder: '{}: {} ({})'" .format(response.status_code, bodyAsJson.get('title'), bodyAsJson.get('message'), bodyAsJson.get('problemCode'))) self.display.error("Received status '{}' from ConfigSeeder: '{}', return empty result.".format(response.status_code, bodyAsJson)) return {} data = {} for configurationValue in response.json(): data[configurationValue['key']] = configurationValue['value'] self.display.v('Loading data from ConfigSeeder...done') return data def createRequest(self, configuration): request = { "tenantKey": configuration.tenantKey, "environmentKey": configuration.environmentKey, "configurationGroupKeys": configuration.configurationGroupKeys , "fullForNonFiltered": False } if configuration.context is not None and len(configuration.context) > 0: request['context'] = configuration.context if configuration.version is not None and len(configuration.version) > 0: request['version'] = configuration.version if configuration.dateTime is not None and len(configuration.dateTime) > 0: request['dateTime'] = configuration.dateTime else: now = datetime.datetime.now() request['dateTime'] = now.strftime("%Y-%m-%dT%H:%M:%S") return request def createHeaders(self, configuration): hostname = platform.node() identificationHash = hashlib.sha256(hostname.encode('UTF-8')).hexdigest() userAgent = 'AnsiblePlugin/' + CONFIGSEEDER_PLUGIN_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')' headers = { 'Authorization': 'Bearer ' + configuration.apiKey, 'Accept': 'application/metadata+json', 'Content-Type': 'application/json', 'X-Hostname': hostname, 'X-Host-Identification': identificationHash, 'X-User-Agent': userAgent } return headers class ConfigSeederConfiguration: def __init__(self, apiKey, url, timeout, tenantKey, environmentKey, configurationGroupKeys, context, version, dateTime): self.apiKey = apiKey self.url = url self.timeout = timeout self.tenantKey = tenantKey self.environmentKey = environmentKey self.configurationGroupKeys = configurationGroupKeys self.context = context self.version = version self.dateTime = dateTime def __repr__(self): return { 'apiKey': 'masked', 'url': str(self.url), 'timeout': str(self.timeout), 'tenantKey': str(self.tenantKey), 'environmentKey': str(self.environmentKey), 'configurationGroupKeys': self.configurationGroupKeys, 'context': self.context, 'version': self.version, 'dateTime': self.dateTime }