Giuseppe Lavagetto has submitted this change and it was merged.
Change subject: service::node: auto-monitoring of local endpoints
......................................................................
service::node: auto-monitoring of local endpoints
Bug: T94821
Change-Id: I96ddc75be235d6efeb2e8d50f4af5bf0f8b2537a
---
A modules/service/Rakefile
A modules/service/files/checker.py
A modules/service/manifests/monitoring.pp
M modules/service/manifests/node.pp
A modules/service/spec/checker/test.json
A modules/service/spec/checker/test_checker.py
A modules/service/spec/checker/test_error_spec.json
7 files changed, 840 insertions(+), 6 deletions(-)
Approvals:
Mobrovac: Looks good to me, but someone else must approve
Giuseppe Lavagetto: Looks good to me, approved
jenkins-bot: Verified
diff --git a/modules/service/Rakefile b/modules/service/Rakefile
new file mode 100644
index 0000000..c3293b0
--- /dev/null
+++ b/modules/service/Rakefile
@@ -0,0 +1,40 @@
+require 'rake'
+require 'fileutils'
+
+require 'rspec/core/rake_task'
+
+modulename = File.basename(File.expand_path(File.dirname(__FILE__)))
+
+
+symlinks = {
+ 'spec/checker/checker.py' => '../../files/checker.py'
+ }
+
+
+task :setup do
+ # TODO: create a virtualenv to run this all
+ symlinks.each do |x|
+ if !File.exist?(x[0])
+ FileUtils.ln_s(x[1], x[0])
+ end
+ end
+end
+
+task :teardown do
+ symlinks.each { |x| FileUtils.rm(x[0], :force => true) }
+end
+
+RSpec::Core::RakeTask.new(:realspec) do |t|
+ t.fail_on_error = false
+ t.pattern = 'spec/*/*_spec.rb'
+end
+
+task :test_checker do
+ puts "Testing the python service checker"
+ sh "nosetests spec/checker/"
+end
+
+task :spec_standalone => [ :setup, :test_checker, :realspec, :teardown]
+
+task :default => :spec_standalone do
+end
diff --git a/modules/service/files/checker.py b/modules/service/files/checker.py
new file mode 100644
index 0000000..af64677
--- /dev/null
+++ b/modules/service/files/checker.py
@@ -0,0 +1,433 @@
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+import json
+import urllib3
+import sys
+import argparse
+import re
+import urllib
+from collections import namedtuple
+
+
+class CheckServiceError(Exception):
+
+ """
+ Generic Exception used as a catchall
+ """
+ pass
+
+
+def fetch_url(client, url, **kw):
+ """
+ Standalone function to fetch an url.
+
+ Args:
+ client (urllib3.Poolmanager):
+ The HTTP client we want to use
+ url (str): The URL to fetch
+
+ kw: any keyword arguments we want to pass to
+ urllib3.request.RequestMethods.request
+ """
+ if 'method' in kw:
+ method = kw['method']
+ else:
+ method = 'GET'
+
+ try:
+ return client.request(
+ method,
+ url,
+ **kw
+ )
+ except urllib3.exceptions.SSLError:
+ raise CheckServiceError("Invalid certificate")
+ except (urllib3.exceptions.ConnectTimeoutError,
+ urllib3.exceptions.TimeoutError,
+ # urllib3.exceptions.ConnectionError, # commented out until we can
+ # remove trusty (aka urllib3 1.7.1) support
+ urllib3.exceptions.ReadTimeoutError):
+ raise CheckServiceError("Timeout on connection while "
+ "downloading {}".format(url))
+ except Exception as e:
+ raise CheckServiceError("Generic connection error: {}".format(e))
+
+
+class CheckService(object):
+
+ """
+ Shell class for checking services
+ """
+ nagios_codes = ['OK', 'WARNING', 'CRITICAL']
+ spec_url = '/?spec'
+ default_response = {'status': 200}
+
+ def __init__(self, host_ip, base_url, timeout=5):
+ """
+ Initialize the checker
+
+ Args:
+ host_ip (str): The host ipv4 address (also works with a hostname)
+
+ base_url (str): The base url the service expects to respond from
+
+ timeout (int): Number of seconds to wait for each request
+ """
+ self.host_ip = host_ip
+ self.base_url = urlparse.urlsplit(base_url)
+ http_host_port = self.base_url.netloc.split(':')
+ if len(http_host_port) < 2:
+ if self.base_url.scheme == 'https':
+ http_host_port.append('443')
+ else:
+ http_host_port.append('80')
+ self.http_host, self.port = http_host_port
+ self._url_prefix = self.base_url.path
+ self.endpoints = {}
+ self._timeout = timeout
+
+ @property
+ def _url(self):
+ """
+ Returns an url pointing to the IP of the host to check.
+ """
+ return "{}://{}:{}{}".format(self.base_url.scheme,
+ self.host_ip,
+ self.port,
+ self._url_prefix)
+
+ def get_endpoints(self):
+ """
+ Gets the full spec from base_url + '/?spec' and parses it.
+ Returns a generator iterating over the available endpoints
+ """
+ http = self._spawn_downloader()
+ # TODO: cache all this.
+ response = fetch_url(
+ http,
+ self._url + self.spec_url,
+ timeout=self._timeout,
+ headers={'Host': self.http_host}
+ )
+
+ resp = response.data.decode('utf-8')
+
+ try:
+ r = json.loads(resp)
+ except ValueError:
+ raise ValueError("No valid spec found")
+
+ TemplateUrl.default = r.get('x-default-params', {})
+ for endpoint, data in r['paths'].items():
+ if not endpoint:
+ continue
+ try:
+ d = data['get']
+ # If x-monitor is False, skip this
+ if not d.get('x-monitor', True):
+ continue
+ default_example = {
+ 'request': {},
+ 'response': self.default_response
+ }
+ examples = d.get('x-amples', [default_example])
+ for x in examples:
+ yield endpoint, x
+ except KeyError:
+ # No GET
+ pass
+
+ def run(self):
+ """
+ Runs the checks on all the endpoints we find
+ """
+ res = []
+ status = 'OK'
+ idx = self.nagios_codes.index(status)
+ try:
+ for endpoint, data in self.get_endpoints():
+ ep_status, msg = self._check_endpoint(endpoint, data)
+ if ep_status != 'OK':
+ res.append("{} is {}: {}".format(endpoint, ep_status, msg))
+ ep_idx = self.nagios_codes.index(ep_status)
+ if ep_idx >= idx:
+ status = ep_status
+ idx = ep_idx
+ message = u"; ".join(res)
+ if status == 'OK':
+ message = "All endpoints are healty"
+ except Exception as e:
+ message = "Generic error: {}".format(e)
+ status = 'CRITICAL'
+ print message
+ sys.exit(self.nagios_codes.index(status))
+
+ def _check_endpoint(self, endpoint, data):
+ """
+ Actually performs the checks on each single endpoint
+ """
+ req = data.get('request', {})
+ req['http_host'] = self.http_host
+ er = EndpointRequest(
+ data.get('title',
+ "test for {}".format(endpoint)),
+ self._url,
+ endpoint,
+ req,
+ data.get('response')
+ )
+ er.run(self._spawn_downloader())
+ return (er.status, er.msg)
+
+ def _spawn_downloader(self):
+ """
+ Spawns an urllib3.Poolmanager with the correct configuration.
+ """
+ kw = {
+ # 'retries': 1, uncomment this once we've got rid of trusty
+ 'timeout': self._timeout
+ }
+ kw['ca_certs'] = "/etc/ssl/certs/ca-certificates.crt"
+ kw['cert_reqs'] = 'CERT_REQUIRED'
+ return urllib3.PoolManager(**kw)
+
+
+class EndpointRequest(object):
+
+ """
+ Manages a request to a specific endpoint
+ """
+
+ def __init__(self, title, base_url, endpoint, request, response):
+ """
+ Initialize the endpoint request
+
+ Args:
+ title (str): a descriptive name
+
+ base_url (str): the base url
+
+ endpoint (str): an url template for the endpoint, per RFC 6570
+
+ request (dict): All data for building the request
+
+ response (dict): What we should test in the response
+ """
+ self.status = 'OK'
+ self.msg = 'Test "{}" healthy'.format(title)
+ self.title = title
+ self._request(request)
+ self._response(response)
+ self.tpl_url = TemplateUrl(base_url + endpoint)
+
+ def run(self, client):
+ """
+ Perform the request, and test the result
+
+ Args:
+ client (urllib3.Poolmanager): the HTTP client we want to use
+ """
+ try:
+ url = self.tpl_url.realize(self.url_parameters)
+ r = fetch_url(
+ client,
+ url,
+ headers=self.request_headers,
+ fields=self.query_parameters,
+ redirect=False
+ )
+ except CheckServiceError as e:
+ return ('CRITICAL', "Could not fetch url {}: {}".format(
+ url, e))
+
+ # Response status
+ if r.status != self.resp_status:
+ self.status = "CRITICAL"
+ self.msg = ("Test {} returned "
+ "the unexpected status {} (expecting: {})".format(
+ self.title, r.status, self.resp_status))
+ return
+
+ # Headers
+ for k, v in self.headers.items():
+ h = r.getheader(k)
+ if h is None or not v(h):
+ self.status = "CRITICAL"
+ self.msg = ("Test {} had an unexpected value "
+ "for header {}: {}".format(self.title, k, h))
+ return
+ # Body
+ if self.body is not None:
+ body = r.data.decode('utf-8')
+ if isinstance(self.body, dict) or isinstance(self.body, list):
+ data = json.loads(body)
+ try:
+ self._check_json_chunk(data, self.body)
+ except CheckServiceError:
+ return
+ except Exception as e:
+ self.status = "CRITICAL"
+ self.msg = ("Test {} responds with malformed "
+ "body: {}".format(self.title, e))
+ else:
+ check = self._verify(self.body)
+ if not check(body):
+ self.status = "WARNING"
+ self.msg = ("Test {} responds with unexpected "
+ "body: {} != {}".format(
+ self.title,
+ body,
+ self.body))
+ return
+
+ def _request(self, data):
+ """
+ Gather data from the request object
+ """
+ self.request_headers = {'Host': data['http_host']}
+ if 'headers' in data:
+ self.request_headers.update(data['headers'])
+ self.url_parameters = data.get('params', {})
+ self.query_parameters = data.get('query', {})
+
+ def _response(self, data):
+ """
+ Organize the expected response data
+ """
+ self.resp_status = data['status']
+ self.body = data.get('body', None)
+ self.headers = {}
+ try:
+ for k, v in data['headers'].items():
+ self.headers[k] = self._verify(v)
+ except KeyError:
+ pass
+
+ def _verify(self, arg):
+ """
+ Return a lambda function to verify the response data
+
+ Args:
+ arg (str): The argument to check against. If enclosed
+ in slashes, it's assumed to be a regex
+ """
+ t = 'eq'
+ if arg.startswith('/') and arg.endswith('/'):
+ arg = arg.strip('/')
+ t = 're'
+ if t == 'eq':
+ return lambda x: (x == arg) or x.startswith(arg)
+ elif t == 're':
+ return lambda x: re.search(arg, x)
+
+ def _check_json_chunk(self, data, model, prefix=''):
+ """
+ Recursively check a json chunk of the response.
+
+ Args:
+ data (mixed): the data to check
+
+ model (mixed): the model to check the data against
+
+ prefix (str): the depth we're checking at
+ """
+ if isinstance(model, dict):
+ for k, v in model.items():
+ p = prefix + '/' + k
+ d = data.get(k, None)
+ self._check_json_chunk(d, v, prefix=p)
+ elif isinstance(model, list):
+ for i in range(len(model)):
+ p = prefix + '[%d]' % i
+ self._check_json_chunk(data[i], model[i], prefix=p)
+ else:
+ check = self._verify(model)
+ if not check(str(data)):
+ self.status = "WARNING"
+ self.msg = ("Test {} responds with "
+ "unexpected body: {} => {}".format(
+ self.title, prefix, data))
+ raise CheckServiceError("{} => {}".format(prefix, data))
+ return True
+
+
+class TemplateUrl(object):
+
+ """
+ A very partial implementation of RFC 6570, limited to our use
+ """
+ transforms = {
+ 'simple': lambda x: x,
+ 'optional': lambda x: '/' + x,
+ 'multiple': lambda x: '/'.join(x)
+ }
+ default = {}
+ base = re.compile('(\{.+?\})', re.U)
+
+ def __init__(self, url_string):
+ """
+ Initialize the template
+
+ Args:
+ url_string (str): The url template
+ """
+ Token = namedtuple('Token', ['key', 'types', 'original'])
+ self._url_string = url_string
+ self.tokens = []
+ for param in self.base.findall(self._url_string):
+ types = ['simple']
+ key = param.strip('{}')
+ if key.startswith('/'):
+ types.append('optional')
+ key = key.lstrip('/')
+ if key.startswith('+'):
+ types.append('multiple')
+ key = key.lstrip('+')
+ self.tokens.append(Token(original=param, key=key, types=types))
+
+ def realize(self, params):
+ """
+ Returns an url based on the template.
+
+ Args:
+ params (dict): the list of params to substitute in the template
+ """
+ realized = self._url_string
+ p = {}
+ p.update(self.default)
+ p.update(params)
+ for token in self.tokens:
+ if token.key in p:
+ v = p[token.key]
+ if isinstance(v, list):
+ v = map(urllib.quote_plus, map(str, v))
+ else:
+ v = urllib.quote_plus(str(v))
+ for transform in reversed(token.types):
+ v = self.transforms[transform](v)
+ else:
+ v = u""
+ realized = realized.replace(
+ token.original, v, 1)
+
+ return realized
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Checks the availability and response of one WMF service')
+ parser.add_argument('host_ip', help="The IP address of the host to check")
+ parser.add_argument('service_url',
+ help="The base url for the service, including port")
+ parser.add_argument('-t', dest="timeout", default=5, type=int,
+ help="Timeout (in seconds) for each "
+ "request. Default: 5")
+ args = parser.parse_args()
+ checker = CheckService(args.host_ip, args.service_url, args.timeout)
+ checker.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/modules/service/manifests/monitoring.pp
b/modules/service/manifests/monitoring.pp
new file mode 100644
index 0000000..4b3e7c5
--- /dev/null
+++ b/modules/service/manifests/monitoring.pp
@@ -0,0 +1,16 @@
+# === Class service::monitoring
+#
+# this is intended to include all shared resources used for monitoring
+# services defined via service::node
+
+class service::monitoring {
+ require_packages 'python-yaml', 'python-urllib3'
+
+ file { '/usr/local/lib/nagions/plugins/service_checker':
+ ensure => present,
+ owner => root,
+ group => root,
+ mode => '0555',
+ source => 'puppet:///modules/service/checker.py',
+ }
+}
diff --git a/modules/service/manifests/node.pp
b/modules/service/manifests/node.pp
index c348e9d..47c5306 100644
--- a/modules/service/manifests/node.pp
+++ b/modules/service/manifests/node.pp
@@ -23,7 +23,16 @@
# ulimit. Default: 10000
#
# [*healthcheck_url*]
-# The url to monitor the service at. 200 OK is the expected answer
+# The url to monitor the service at. 200 OK is the expected
+# answer. If has_spec it true, this is supposed to be the base url
+# for the spec request
+#
+# [*firejail*]
+# Whether to use/enable firejail or not
+#
+# [*has_spec*]
+# If the service specifies a swagger spec, use it to thoroughly
+# monitor it
#
# === Examples
#
@@ -50,6 +59,7 @@
$no_file = 10000,
$healthcheck_url='/_info',
$firejail = false,
+ $has_spec = false,
) {
# Import all common configuration
include service::configuration
@@ -159,10 +169,21 @@
port => $port,
}
- # Basic monitoring
- monitoring::service { $title:
- description => $title,
- check_command => "check_http_port_url!${port}!${healthcheck_url}",
+ if $has_spec {
+ # Advanced monitoring
+ include service::monitoring
+
+ $monitor_url = "http://${::ipaddress}:${port}${healthcheck_url}"
+ nrpe::monitor_service{ "endpoints_${title}":
+ description => "${title} endpoints health",
+ nrpe_command => "/usr/local/lib/nagios/plugins/service_checker -t
5 ${::ipaddress} ${monitor_url}",
+ subscribe =>
File['/usr/local/lib/nagions/plugins/service_checker'],
+ }
+ } else {
+ # Basic monitoring
+ monitoring::service { $title:
+ description => $title,
+ check_command => "check_http_port_url!${port}!${healthcheck_url}",
+ }
}
}
-
diff --git a/modules/service/spec/checker/test.json
b/modules/service/spec/checker/test.json
new file mode 100644
index 0000000..edb8394
--- /dev/null
+++ b/modules/service/spec/checker/test.json
@@ -0,0 +1,22 @@
+{
+ "basepath": "/api",
+ "x-default-params": {"who": "joe"},
+ "paths": {
+ "/simple": { "get": {} },
+ "/not_monitored": {"get": {"x-monitor": false}},
+ "/{who}/{verb}": {"get": {
+ "x-amples": [{
+ "request": {
+ "params": {
+ "verb": "rulez"
+ }
+ },
+ "response": {
+ "body": "\"For sure!\"",
+ "status": 200
+ },
+ "title": "General affirmation"
+ }]
+ }}
+ }
+}
diff --git a/modules/service/spec/checker/test_checker.py
b/modules/service/spec/checker/test_checker.py
new file mode 100644
index 0000000..2cba645
--- /dev/null
+++ b/modules/service/spec/checker/test_checker.py
@@ -0,0 +1,280 @@
+import checker
+import unittest
+import mock
+import json
+import os
+import urllib3
+import copy
+
+
+class TestTemplateUrl(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.t = checker.TemplateUrl(
+ 'https://example.org/test/{where}{/what}{/+why}')
+
+ def test_init(self):
+ """
+ Test initialization of the template url
+ """
+ origs = [el.original for el in self.t.tokens]
+ self.assertEquals(origs, ['{where}', '{/what}', '{/+why}'])
+ self.assertEquals(self.t.tokens[0].types, ['simple'])
+ self.assertEquals(self.t.tokens[1].types, ['simple', 'optional'])
+
+ def test_realize(self):
+ """
+ Test Realization
+ """
+ params = {'where': 'Rome', 'what': 'Eat', 'why': ['I', 'am', 'hungry']}
+ self.assertEquals(self.t.realize(params),
+ 'https://example.org/test/Rome/Eat/I/am/hungry')
+
+ params['what'] = 'Eat:pasta'
+ self.assertEquals(
+ self.t.realize(params),
+ 'https://example.org/test/Rome/Eat%3Apasta/I/am/hungry')
+ params1 = {'where': 'Rome'}
+ self.assertEquals(self.t.realize(params1),
+ 'https://example.org/test/Rome')
+
+ del params['why']
+ self.assertEquals(self.t.realize(params),
+ 'https://example.org/test/Rome/Eat%3Apasta')
+
+
+class TestEndpointRequest(unittest.TestCase):
+
+ def mock_response(self, d):
+ r = mock.create_autospec(urllib3.response.HTTPResponse)
+ r.status = d['status']
+ r.data = json.dumps(d['body']).encode('utf-8')
+
+ def getheader(key):
+ return d['headers'].get(key, None)
+ r.getheader = mock.MagicMock(side_effect=getheader)
+ checker.fetch_url = mock.MagicMock(return_value=r)
+
+ def setUp(self):
+ self.resp = {
+ 'body': {
+ 'items': [
+ {
+ 'comment': '/.*/',
+ 'rev': '/\\d+/',
+ 'tid': '/^[0-9a-fA-F]{4}-[0-9a-fA-F]{4}$/',
+ 'title': 'Foobar'
+ }
+ ]
+ },
+ 'headers': {
+ 'content-type': 'application/json',
+ 'etag': '/.+/'
+ },
+ 'status': 200
+ }
+
+ self.ep = checker.EndpointRequest(
+ "a Test endpoint",
+ "http://127.0.0.1/baseurl",
+ "/an/endpoint/{revision}",
+ {"http_host": 'example.org', 'params': {'revision': 1}},
+ copy.deepcopy(self.resp)
+ )
+ self.resp["body"]["items"] = [
+ {"title": "Foobar",
+ "comment": "blabla",
+ "rev": 1,
+ "tid": "00AA-abcd"}
+ ]
+ self.resp["headers"]["etag"] = "Imtrackingyou"
+
+ def test_init(self):
+ """
+ Test initialization
+ """
+ self.assertEquals(self.ep.title, "a Test endpoint")
+ self.assertEquals(self.ep.tpl_url._url_string,
+ "http://127.0.0.1/baseurl/an/endpoint/{revision}")
+ self.assertEquals(self.ep.request_headers, {'Host': 'example.org'})
+ self.assertEquals(self.ep.url_parameters, {'revision': 1})
+ self.assertEquals(self.ep.resp_status, 200)
+ self.assertTrue(self.ep.headers["content-type"]('application/json'))
+
+ def test_run_ok(self):
+ """
+ Test a successful run
+ """
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'OK')
+
+ def test_run_bad_status(self):
+ """
+ Test an unexpected HTTP status
+ """
+ self.resp['status'] = 301
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'CRITICAL')
+ self.assertEquals("Test a Test endpoint returned "
+ "the unexpected status 301 (expecting: 200)",
+ self.ep.msg)
+
+ def test_run_bad_header(self):
+ """
+ Test an unexpected HTTP Header
+ """
+ self.resp['headers']['etag'] = ""
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'CRITICAL')
+ self.assertEquals("Test a Test endpoint had an unexpected value "
+ "for header etag: ", self.ep.msg)
+
+ def test_run_missing_header(self):
+ """
+ Test a missing HTTP header
+ """
+ del self.resp['headers']['etag']
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'CRITICAL')
+ self.assertEquals("Test a Test endpoint had an unexpected value "
+ "for header etag: None", self.ep.msg)
+
+ def test_run_bad_body(self):
+ """
+ Test unexpected value in body
+ """
+ self.resp['body']['items'][0]['tid'] = 12
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'WARNING')
+ self.assertEquals("Test a Test endpoint responds with unexpected "
+ "body: /items[0]/tid => 12", self.ep.msg)
+
+ def test_run_missing_body(self):
+ """
+ Test missing value in body
+ """
+ del self.resp['body']['items'][0]['tid']
+ self.mock_response(self.resp)
+ self.ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'WARNING')
+ self.assertEquals("Test a Test endpoint responds with unexpected "
+ "body: /items[0]/tid => None", self.ep.msg)
+
+ def test_default_response(self):
+ """
+ Test a simple endpoint
+ """
+ ep = checker.EndpointRequest(
+ "simple test",
+ "http://127.0.0.1:7321",
+ "/test",
+ {'http_host': "example.org"},
+ {'status': 200},
+ )
+ self.mock_response(
+ {"status": 200,
+ "body": "Hello, World!",
+ "headers": {"content-length": 3240}})
+ ep.run(urllib3.PoolManager())
+ self.assertEquals(self.ep.status, 'OK')
+
+
+class TestCheckService(unittest.TestCase):
+ routes = {}
+
+ def add_mock_response(self, route, d):
+ r = mock.create_autospec(urllib3.response.HTTPResponse)
+ r.status = d['status']
+ r.data = json.dumps(d['body']).encode('utf-8')
+
+ def getheader(key):
+ return d['headers'].get(key, None)
+ r.getheader = mock.MagicMock(side_effect=getheader)
+ self.routes[route] = r
+
+ def router(self, client, route, **kw):
+ r = route.replace(self.cs._url, '')
+ return self.routes.get(r, None)
+
+ def mock_routes(self):
+ checker.fetch_url = mock.MagicMock(side_effect=self.router)
+
+ def setUp(self):
+ self.cs = checker.CheckService('127.0.0.1', 'http://example.org/api')
+ fn = os.path.join(os.path.dirname(__file__), 'test.json')
+ with open(fn, 'rb') as f:
+ data = f.read().encode('utf-8')
+ self.add_mock_response(
+ '/?spec', {'status': 200, 'body': json.loads(data)})
+
+ def test_initialize(self):
+ """
+ Test initialization
+ """
+ self.assertEquals(self.cs.host_ip, '127.0.0.1')
+ self.assertEquals(self.cs._timeout, 5)
+ self.assertEquals(self.cs.port, '80')
+ self.assertEquals(self.cs.http_host, 'example.org')
+ self.assertEquals(self.cs._url, 'http://127.0.0.1:80/api')
+
+ def test_get_endpoints(self):
+ """
+ Test list of endpoints is returned
+ """
+ self.mock_routes()
+ l = [el for el, data in self.cs.get_endpoints()]
+ self.assertEquals(l, [u'/simple', u'/{who}/{verb}'])
+
+ def test_get_ep_invalid_spec(self):
+ """
+ Test what endpoints are returned with incorrect specs
+ """
+ fn = os.path.join(os.path.dirname(__file__), 'test_error_spec.json')
+ with open(fn, 'rb') as f:
+ data = f.read().encode('utf-8')
+ self.add_mock_response(
+ '/?spec', {'status': 200, 'body': json.loads(data)})
+ self.mock_routes()
+ l = [el for el, _ in self.cs.get_endpoints()]
+ self.assertEquals(l, [u'/{who}/{verb}'])
+
+ def test_run(self):
+ """
+ Test a successful run
+ """
+ self.add_mock_response('/simple', {'status': 200, 'body': 'hi'})
+ self.add_mock_response(
+ '/joe/rulez', {'status': 200, 'body': 'For sure!'})
+ self.mock_routes()
+ with self.assertRaises(SystemExit) as e:
+ self.cs.run()
+ self.assertEquals(e.exception.code, 0)
+
+ def test_endpoint_critical(self):
+ """
+ Test a critical exit
+ """
+ self.add_mock_response('/simple', {'status': 200, 'body': 'hi'})
+ self.add_mock_response('/joe/rulez', {'status': 301, 'body': ''})
+ self.mock_routes()
+ with self.assertRaises(SystemExit) as e:
+ self.cs.run()
+ self.assertEquals(e.exception.code, 2)
+
+ def test_endpoint_warning(self):
+ """
+ Test a warning exit
+ """
+ self.add_mock_response('/simple', {'status': 200, 'body': 'hi'})
+ self.add_mock_response(
+ '/joe/rulez', {'status': 200, 'body': 'For sure?'})
+ self.mock_routes()
+ with self.assertRaises(SystemExit) as e:
+ self.cs.run()
+ self.assertEquals(e.exception.code, 1)
diff --git a/modules/service/spec/checker/test_error_spec.json
b/modules/service/spec/checker/test_error_spec.json
new file mode 100644
index 0000000..8d11544
--- /dev/null
+++ b/modules/service/spec/checker/test_error_spec.json
@@ -0,0 +1,22 @@
+{
+ "basepath": "/api",
+ "x-default-params": {"who": "joe"},
+ "paths": {
+ "/simple": {},
+ "/not_monitored": {"get": {"x-monitor": false}},
+ "/{who}/{verb}": {"get": {
+ "x-amples": [{
+ "request": {
+ "params": {
+ "verb": "rulez"
+ }
+ },
+ "response": {
+ "body": "\"For sure!\"",
+ "status": 200
+ },
+ "title": "General affirmation"
+ }]
+ }}
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/223328
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I96ddc75be235d6efeb2e8d50f4af5bf0f8b2537a
Gerrit-PatchSet: 16
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Filippo Giunchedi <[email protected]>
Gerrit-Reviewer: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Mobrovac <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits