diff --git a/.travis.yml b/.travis.yml index f29453b..9cd1ee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,28 @@ language: python -sudo: required python: - - "2.7" -install: +- '2.6' +stages: +- linter +- test-basic-auth +jobs: + include: + - stage: linter + install: - pip install -r requirements.txt -script: - - make check -services: - - docker + script: + - make syntax + - stage: test-basic-auth + install: + - pip install -r requirements.txt + env: + - secure: Bv8kXVD9Zy2+nSVcIUcGwupC/3/0hJIzpfsPk9+4fpDKARBxMdCv5AsshoPr6HbuVKucOCALd9sSyTTlTEjOSVGwfmYDvlxtq3tG5b1l0dD6wyoE70B2SzQ+oGcAasufXPe+wrPJbnygG3j8yLZ3IKeSzWkpuDkAc2TxWIPlbrYHTGu2IfouhCxDtZavbxzSQ/U5tXIUdqq7g5Oeb6W0Au8TalAsnVl0bKjTNp0tbxfMJ6DXQNYj20pdpom/Fb2uTY6SA9xiTBLLyrTc1tM6MvYHYtLCTHLLNIcZoEgbug6Bo5lbXp//ODBCzNiktof/Tg9tvtHQLL0nVGWVtHnnYwFn2SfPqq9uCN3YNga9dZwHaLhhJhMTK5tcVdGagyawPLkcjtcRfW//CnWGJg5ZP1SlPnMHANy86esUQb6tqECn0HaNEhBHlvGzDCmHb71R0eS0zHFvmlabnzpm+uIGEI5DFzbeNKAy0pHE8YNrIuoqyQJc5hu32DjYZbozFED4JHVezRn6yuzArRXN/3sTeJYhtW9Jzn3wKejXDw06xOZnbFcvxH9De4MvQ7MW2iI8vQxTY46I22Vxs+ouXtE/x0yUNSfA6ERDl6zvc4QAnaB8ScoLvhymSCYw5VcA+PNKfCaY7lgumYSs3iwc+l12fb5Dt56Qo1WuJexLAxcXaMA= + - secure: onS72up7xaCe+AQ+WGXO51oyCSzAMUHWIH8Yf9DOLXWI3SoGE2a+o96TBmHKpne1+oQFHxB1QVPiOlCefdmeDbphwhvzjl6DxMZXdNgS71YAzRzcg8G5ChE9LZZZrRBXcUu235OFxG6m9dkPlVN37wIwrN3h+g+KBwpBpcJoV7Z8uS0VngYlH3wFudAsQ1fCgq8hfoFQmm6Ji9UD9BxAwzGX2zsHdslwhDKubBJY5WL+c5Sc5rjOzVLh+nFEEvbw0++zVCC7mMs8wX97EVaacAn6xbykzKuEqKMcheDIYB6BivAJAjPW+TKzo5i8MblAL0wJQJrIn6/2DiyMnAD7xoEhc1myh+vX9RH63det+GwYrPLjHabya18IIuOOxaa0I0fAtygQsHnbfnlnDxZp3/EzhCtpcs8H9C8QCs+kTkWjyv5LikJDGKCyYL0KHfkpPKpFCqKbZk+VXfsqJIWQlUrJUdd6cijcmPK3rt2Wj3tjLwf2y+Y1JkbnM5UCNq8qJk+YaQUbs+UHi3GtKpt1wQlvKHNQj2OkYvc+dpVPeDW43kNjJ7eiEa27DBVCCpCA95OrAbxNbvYf+Xb80ZaxmHkU6K+XlWg7S6NlrSp315UI6hHNQzYZ3Mw3NqR55l9tK4YoTKt+TlpFhazPODK/S1dgZvCMbPk0NwBZTMG1k0k= + script: + - make travis-check + - stage: test-token + install: + - pip install -r requirements.txt + env: + - secure: OKohVOEy2nBGdk4qsIVNVBnh/lcDpiXsL76ssgfKLnp2VOal/CjytwnpLw7c48cX9ICMnvNiK8ZixAQ0McXx1rI9FGVpX/9GDqNKN6p/lr/OBGn3QPGtjUehUVeKDjBPi2g28Sd6EEiUdMG2oauBErjX6QzDZ0mFrYwoNKzmuExve1Zbyqaom47Quzx6OdWEk9AuvMC10gj3bObjX05wm+GvZHRPv+InIn2Qd1l4HzC4e4YhYWN+znLo9z18G1b6JXeA1Bdp3pTfXRb/SxRUwjB6pG3tu3O7dHtm9ZzIzJgY3uY0Xtrt2q1ypeu84LMru8g0pyO8UAe7F77afw7xoxeoTE9uxY3soZcqjzhRV+a2lc3dpHQv0r6rPNHh4W2lEncEtj/SCasSr3jsv0KUtZdqe+1882P+gN+bzMnmGxp3wO7bRGQUAb6dsbvL1GmFU6g7eCOqQ3f/P6tFNtZdbIGWekN1M/lpCrlkQ8+rd9kCC8ntrdx52ROWB4yaM1x6XXAnDjOPNf//X5FpM1xO+w2JrjdRQEBMqX2oTz74BIJYkaUZevyFg4OKGUGLXUsHL1+wd/5fM8nWnmMhiTq5u7g90hnpXU/+8GRD1D36woVbg6oS+xstfSXnC7TkkvlJKWQXB80kOIaeVwMrs5MhMQ91HTle8eUZ9YS0Aw37bzo= + script: + - make travis-check diff --git a/Makefile b/Makefile index bdafdde..1d4a4ff 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,14 @@ syntax: container: tools/start_grafana.sh -check: container +test-local-token: container GRAFANA_API_KEY='$(shell python tools/get_or_create_token.py)' ansible-playbook test.yml - #TODO: Check annotations are available in grafana - #TODO: use a test framework like unittest + +test-local-basic-auth: container + GRAFANA_USER='admin' GRAFANA_PASSWORD='admin' ansible-playbook test.yml + +local-check: test-local-token test-local-basic-auth + +# This test will be called multiple times by travis-ci with different env variables. +travis-check: + ansible-playbook test.yml diff --git a/callback_plugins/grafana_annotations.py b/callback_plugins/grafana_annotations.py index 6dc6903..0c4907a 100644 --- a/callback_plugins/grafana_annotations.py +++ b/callback_plugins/grafana_annotations.py @@ -10,12 +10,7 @@ from base64 import b64encode from datetime import datetime -try: - import httplib -except ImportError: - # Python 3 - import http.client as httplib - +from ansible.module_utils.urls import open_url from ansible.plugins.callback import CallbackBase DOCUMENTATION = ''' @@ -28,11 +23,21 @@ requirements: - whitelisting in configuration options: - grafana_host: - description: Grafana server address and port + grafana_url: + description: Grafana annotations api URL + env: + - name: GRAFANA_URL + default: http://127.0.0.1:3000/api/annotations + grafana_validate_certs: + description: (bool) validate the SSL certificate of the Grafana server. (For HTTPS url) env: - - name: GRAFANA_HOST - default: 127.0.0.1:3000 + - name: GRAFANA_VALIDATE_CERT + default: True + http_agent: + description: The HTTP 'User-agent' value to set in HTTP requets. + env: + - name: HTTP_AGENT + default: 'Ansible (grafana_annotations callback)' api_key: description: Grafana API key, allowing to authenticate when posting on the HTTP API. If not provided, grafana_login and grafana_password will @@ -93,6 +98,15 @@ def to_millis(dt): return int(dt.strftime('%s')) * 1000 +def str2bool(string): + if string in [True, 'True', 'true', '1', 'yes']: + return True + elif string in [False, 'False', 'false', '0', 'no']: + return True + else: + raise Exception("Unsupported value '%s' as boolean" % string) + + class CallbackModule(CallbackBase): """ ansible grafana callback plugin @@ -110,27 +124,27 @@ class CallbackModule(CallbackBase): def __init__(self): super(CallbackModule, self).__init__() - self.grafana_host = os.getenv('GRAFANA_HOST', '127.0.0.1:3000') - self.secure = int(os.getenv('GRAFANA_SECURE', 0)) - if self.secure not in [0, 1]: - self.secure = 0 api_key = os.getenv('GRAFANA_API_KEY', None) - grafana_user = os.getenv('GRAFANA_USER', None) - grafana_password = os.getenv('GRAFANA_PASSWORD', '') + self.grafana_url = os.getenv('GRAFANA_URL', 'http://127.0.0.1:3000/api/annotations') + self.grafana_validate_certs = str2bool(os.getenv('GRAFANA_VALIDATE_CERT', True)) + self.http_agent = os.getenv('HTTP_AGENT', 'Ansible (grafana_annotations callback)') + self.grafana_user = os.getenv('GRAFANA_USER', None) + self.grafana_password = os.getenv('GRAFANA_PASSWORD', None) self.dashboard_id = os.getenv('GRAFANA_DASHBOARD_ID', None) self.panel_id = os.getenv('GRAFANA_PANEL_ID', None) + self.force_basic_auth = False + + self.headers = {'Content-Type': 'application/json'} if api_key: - authorization = "Bearer %s" % api_key - elif grafana_user is not None: - authorization = "Basic %s" % b64encode("%s:%s" % (grafana_user, grafana_password)) + self.headers['Authorization'] = "Bearer %s" % api_key + elif self.grafana_user is not None and self.grafana_password is not None: + self.force_basic_auth = True else: self.disabled = True self._display.warning("Authentcation required, please set GRAFANA_API_KEY or GRAFANA_USER/GRAFANA_PASSWORD") return - self.headers = {'Content-Type': 'application/json', - 'Authorization': authorization} self.errors = 0 self.hostname = socket.gethostname() self.username = getpass.getuser() @@ -197,11 +211,10 @@ def v2_runner_on_failed(self, result, **kwargs): self._send_annotation(json.dumps(data)) def _send_annotation(self, annotation): - if int(self.secure) == 1: - self.http = httplib.HTTPSConnection(self.grafana_host) - else: - self.http = httplib.HTTPConnection(self.grafana_host) - self.http.request("POST", "/api/annotations", annotation, self.headers) - response = self.http.getresponse() - if response.status != 200: - self._display.warning("Grafana server responded with HTTP %d" % response.status) + try: + response = open_url(self.grafana_url, data=annotation, headers=self.headers, + method="POST", validate_certs=self.grafana_validate_certs, + url_username=self.grafana_user, url_password=self.grafana_password, + http_agent=self.http_agent, force_basic_auth=self.force_basic_auth) + except Exception as e: + self._display.warning('Could not submit message to Grafana: %s' % str(e))