#!/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 tenantKey: description: Name of the tenantKey default: default env: - name: CONFIGSEEDER_TENANTKEY ini: - section: configseeder key: tenantKey required: True 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 requests from ansible.plugins.lookup import LookupBase from ansible.utils.display import Display # TODO # - Verify Error handling (when accessing configseeder) # - Retry if configseeder can't be reached # - Caching of received data (per filter criteria) # Every Plugin is started as a new process. In-memory caching therefor isn't possible. # Caching would require to store data on the filesystem or in another process # - Validate date format for dateTime # - Set CONFIGSEEDER_PLUGIN_VERSION by gitlab pipeline to used tag for tagbuilds (otherwise set date) CONFIGSEEDER_PLUGIN_VERSION='0.1.0' 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') 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: ' + configuration.url) self.display.vvv('\t\tEnvironmentKey: {}'.format(configuration.environmentKey)) self.display.vvv('\t\tConigGroupKeys: {}'.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') environmentKey = self.get_option('environmentKey') configurationGroupKeys = self.get_option('configurationGroupKeys') if apiKey is None or len(apiKey) == 0: self.display.error("Mandatory ConfigSeeder option 'apiKey' not set. Set in ansible.cfg [configseeder] with key 'apiKey' or with environment variable CONFIGSEEDER_APIKEY") if url is None or len(url) == 0: self.display.error("Mandatory ConfigSeeder option 'url' not set. Set in ansible.cfg [configseeder] with key 'url' or with environment variable CONFIGSEEDER_URL") if environmentKey is None or len(environmentKey) == 0: self.display.error("Mandatory ConfigSeeder option 'environmentKey' not set. Set in ansible.cfg [configseeder] with key 'environmentKey' or with environment variable CONFIGSEEDER_ENVIRONMENTKEY") if configurationGroupKeys is None or len(configurationGroupKeys) == 0: self.display.error("Mandatory ConfigSeeder option 'configurationGroupKeys' not set. Set in ansible.cfg [configseeder] with key 'configurationGroupKeys' or with environment variable CONFIGSEEDER_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') # TODO Validate Format 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") return ConfigSeederConfiguration(apiKey, url, 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 {} 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 } 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") requestJson = json.dumps(request) self.display.vvv('Request: {}'.format(requestJson)) response = requests.post(url=configuration.url + '/public/api/v1/configurations', headers=headers, data=requestJson) self.display.vvv('Response: Code: {}, Message: '.format(response.status_code, response.content)) if response.status_code != 200: errorHeader = response.headers['x-configseeder-error'] self.display.error("Received {} from ConfigSeeder: {}".format(response.status_code, errorHeader)) return {} data = {} for configurationValue in response.json(): data[configurationValue['key']] = configurationValue['value'] self.display.v('Loading data from ConfigSeeder...done') return data class ConfigSeederConfiguration: def __init__(self, apiKey, url, tenantKey, environmentKey, configurationGroupKeys, context, version, dateTime): self.apiKey = apiKey self.url = url 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), 'tenantKey': str(self.tenantKey), 'environmentKey': str(self.environmentKey), 'configurationGroupKeys': self.configurationGroupKeys, 'context': self.context, 'version': self.version, 'dateTime': self.dateTime }