# Copyright (C) 2018-2021 Arthur de Jong # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. """Simple WSGI application to run the munin-plot application.""" import json import os import time import traceback import urllib.parse import pkg_resources from muninplot.data import get_info, get_values # The directory that contains the JSON files that describe the dashboards DASHBOARDS_DIR = os.environ.get('DASHBOARDS_DIR', None) def static_serve(environ, start_response): """Server static files that are shipped with the package.""" path = environ.get('PATH_INFO', '').lstrip('/') or 'index.html' path = os.path.normpath(os.sep + path).lstrip(os.sep) if path.endswith('.html'): content_type = 'text/html; charset=utf-8' elif path.endswith('.js'): content_type = 'text/javascript' elif path.endswith('.css'): content_type = 'text/css' elif path.endswith('.png'): content_type = 'image/png' elif path.endswith('.ico'): content_type = 'image/vnd.microsoft.icon' else: content_type = 'application/octet-stream' csp = "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; " + \ "script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" path = os.path.join('static', path) if not pkg_resources.resource_exists('muninplot', path): start_response('404 NOT FOUND', [ ('Content-Type', 'text/plain'), ('Content-Security-Policy', csp)]) return [b'FILE NOT FOUND'] start_response('200 OK', [ ('Content-Type', content_type), ('Content-Security-Policy', csp)]) return [pkg_resources.resource_stream('muninplot', path).read()] def list_graphs(environ, start_response): """Return the known Munin graphs as JSON.""" start_response('200 OK', [ ('Content-Type', 'application/json')]) return [json.dumps(get_info(), indent=2, sort_keys=True).encode('utf-8')] def list_dashboards(environ, start_response): """Return the configured dashboards as JSON.""" dashboards = {} # Go over DASHBOARDS_DIR and load JSON files from that dashboards_dir = environ.get('DASHBOARDS_DIR', DASHBOARDS_DIR) if dashboards_dir and os.path.isdir(dashboards_dir): for filename in sorted(os.listdir(dashboards_dir)): filename = os.path.join(dashboards_dir, filename) if filename.endswith('.json') and os.path.isfile(filename): try: with open(filename, 'rt') as f: dashboard = json.load(f) dashboard.setdefault('name', os.path.basename(filename)[:-5]) dashboards[dashboard['name']] = dashboard except Exception: traceback.print_exc(file=environ['wsgi.errors']) start_response('200 OK', [ ('Content-Type', 'application/json')]) return [json.dumps(dashboards, indent=2, sort_keys=True).encode('utf-8')] def _field_key(x): """Order field.min, field, field.max.""" if x.endswith('.min'): return x[:-4] + '.0' elif x.endswith('.max'): return x[:-4] + '.2' return x + '.1' def _parse_timestamp(timestamp): """Return a timestamp value from the specified string.""" formats = ( '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%dT%H:%M', '%Y-%m-%d') for fmt in formats: try: return int(time.mktime(time.strptime(timestamp, fmt))) except ValueError: pass raise ValueError('time data %r does not match any known format' % timestamp) def get_data(environ, start_response): """Return a data series for the graph as CSV.""" path = environ.get('PATH_INFO', '').lstrip('/') _, group, host, graph = path.split('/') parameters = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) # get the time range to fetch the data for end = parameters.get('end') end = _parse_timestamp(end[0]) if end else time.time() start = parameters.get('start') start = _parse_timestamp(start[0]) if start else end - 24 * 60 * 60 * 7 # return the values as CSV start_response('200 OK', [ ('Content-Type', 'text/csv')]) for values in get_values(group, host, graph, start, end): if not isinstance(values[0], str): values[0] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(values[0])) yield ('%s\n' % (','.join(str('' if value is None else value) for value in values))).encode('utf-8') def application(environ, start_response): """Serve munin-plot WSGI application.""" # override MUNIN_DBDIR if 'MUNIN_DBDIR' in environ: import muninplot.data muninplot.data.MUNIN_DBDIR = environ['MUNIN_DBDIR'] # get request path path = environ.get('PATH_INFO', '').lstrip('/') if path.startswith('graphs'): return list_graphs(environ, start_response) if path.startswith('dashboards'): return list_dashboards(environ, start_response) elif path.startswith('data/'): return get_data(environ, start_response) else: return static_serve(environ, start_response)