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

Reply via email to