initial commit

This commit is contained in:
Franzi 2020-12-18 21:55:00 +01:00
commit fe58b5a874
Signed by: kunsi
GPG key ID: 12E3D2136B818350
10 changed files with 1149 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
services.json
config.json
*.xml
*.pyc
.venv
*.swp

138
hosted.js Normal file
View file

@ -0,0 +1,138 @@
/*
* info-beamer hosted.js Mockup for local development.
* You can find the latest version of this file at:
*
* https://github.com/info-beamer/package-sdk
*
* BSD 2-Clause License
*
* Copyright (c) 2017-2019 Florian Wesch <fw@info-beamer.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
(function() {
var head = document.getElementsByTagName("head")[0];
var asset_root = "https://cdn.infobeamer.com/s/mock-use-latest/";
function setupResources(js, css) {
for (var idx = 0; idx < js.length; idx++) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.setAttribute("src", asset_root + 'js/' + js[idx]);
head.appendChild(script);
}
for (var idx = css.length-1; idx >= 0; idx--) {
var link = document.createElement('link')
link.setAttribute('rel', 'stylesheet')
link.setAttribute('type', 'text/css')
link.setAttribute('href', asset_root + 'css/' + css[idx])
head.insertBefore(link, head.firstChild);
}
}
var style = document.createElement('style');
var rules = document.createTextNode(
"body { width: 750px; margin: auto; }"
)
style.type = 'text/css';
style.appendChild(rules);
head.appendChild(style);
if (window.MOCK_ASSETS == undefined)
console.error("[MOCK HOSTED.JS] window.MOCK_ASSETS undefined");
if (window.MOCK_NODE_ASSETS == undefined)
console.error("[MOCK HOSTED.JS] window.MOCK_NODE_ASSETS undefined");
if (window.MOCK_DEVICES == undefined)
console.error("[MOCK HOSTED.JS] window.MOCK_DEVICES undefined");
if (window.MOCK_CONFIG == undefined)
console.error("[MOCK HOSTED.JS] window.MOCK_CONFIG undefined");
var ib = {
assets: window.MOCK_ASSETS,
node_assets: window.MOCK_NODE_ASSETS,
config: window.MOCK_CONFIG,
devices: window.MOCK_DEVICES,
doc_link_base: 'data:text/plain,This would have opened the package documentation for ',
apis: {
geo: {
get: function(params) {
if (!params.q) {
console.error("no q parameter for weather query");
}
return new Promise(function(resolve, reject) {
setTimeout(function() { // simulate latency
resolve({"hits":[
{"lat":49.00937,"lon":8.40444,"name":"Karlsruhe (Baden-W\u00fcrttemberg, Germany)"},
{"lat":48.09001,"lon":-100.62042,"name":"Karlsruhe (North Dakota, United States)"}
]})
}, 800);
})
},
}
}
}
ib.setDefaultStyle = function() {
setupResources([], ['reset.css', 'bootstrap.css'])
}
var asset_chooser_response = window.MOCK_ASSET_CHOOSER_RESPONSE
if (asset_chooser_response) {
console.log("[MOCK HOSTED.JS] emulating asset chooser");
ib.assetChooser = function() {
console.log("[MOCK HOSTED.JS] asset chooser mockup returns", asset_chooser_response);
return new Promise(function(resolve) {
resolve(asset_chooser_response);
})
}
}
ib.setConfig = function(config) {
var as_string = JSON.stringify(config);
ib.config = JSON.parse(as_string);
console.log("[MOCK HOSTED.JS] setConfig", as_string);
}
ib.getConfig = function(cb) {
console.warn("[MOCK HOSTED.JS] using .getConfig is deprecated. Use .ready.then(...) instead");
cb(ib.config);
}
ib.getDocLink = function(name) {
return ib.doc_link_base + name;
}
ib.onAssetUpdate = function(cb) {
console.warn("[MOCK HOSTED.JS] onAssetUpdate is a no-op in the mock environment");
}
ib.ready = new Promise(function(resolve) {
console.log("[MOCK HOSTED.JS] ready");
resolve(ib.config);
})
window.infobeamer = window.ib = ib;
})();

231
hosted.lua Normal file
View file

@ -0,0 +1,231 @@
--[[
Part of info-beamer hosted. You can find the latest version
of this file at:
https://github.com/info-beamer/package-sdk
Copyright (c) 2014,2015,2016,2017 Florian Wesch <fw@dividuum.de>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]--
local resource_types = {
["image"] = function(value)
local surface
local image = {
asset_name = value.asset_name,
filename = value.filename,
type = value.type,
}
function image.ensure_loaded()
if not surface then
surface = resource.load_image(value.asset_name)
end
return surface
end
function image.load()
image.ensure_loaded()
local state = surface:state()
return state ~= "loading"
end
function image.get_surface()
return image.ensure_loaded()
end
function image.draw(...)
image.ensure_loaded():draw(...)
end
function image.unload()
if surface then
surface:dispose()
surface = nil
end
end
function image.get_config()
return image
end
return image
end;
["video"] = function(value)
local surface
local video = {
asset_name = value.asset_name,
filename = value.filename,
type = value.type,
}
function video.ensure_loaded(opt)
if not surface then
surface = util.videoplayer(value.asset_name, opt)
end
return surface
end
function video.load(opt)
video.ensure_loaded(opt)
local state = surface:state()
return state ~= "loading"
end
function video.get_surface(opt)
return video.ensure_loaded(opt)
end
function video.draw(...)
video.ensure_loaded():draw(...)
end
function video.unload()
if surface then
surface:dispose()
surface = nil
end
end
function video.get_config()
return video
end
return video
end;
["child"] = function(value)
local surface
local child = {
asset_name = value.asset_name,
filename = value.filename,
type = value.type,
}
function child.ensure_loaded()
if surface then
surface:dispose()
end
surface = resource.render_child(value.asset_name)
return surface
end
function child.load()
return true
end
function child.get_surface()
return child.ensure_loaded()
end
function child.draw(...)
child.ensure_loaded():draw(...)
end
function child.unload()
if surface then
surface:dispose()
surface = nil
end
end
function child.get_config()
return child
end
return child
end;
["json"] = function(value)
return require("json").decode(value)
end;
}
local types = {
["date"] = function(value)
return value
end;
["json"] = function(value)
return value
end;
["text"] = function(value)
return value
end;
["string"] = function(value)
return value
end;
["integer"] = function(value)
return value
end;
["select"] = function(value)
return value
end;
["device"] = function(value)
return value
end;
["boolean"] = function(value)
return value
end;
["duration"] = function(value)
return value
end;
["custom"] = function(value)
return value
end;
["color"] = function(value)
local color = {}
color.r = value.r
color.g = value.g
color.b = value.b
color.a = value.a
color.rgba_table = {color.r, color.g, color.b, color.a}
color.rgba = function()
return color.r, color.g, color.b, color.a
end
color.rgb_with_a = function(a)
return color.r, color.g, color.b, a
end
color.clear = function()
gl.clear(color.r, color.g, color.b, color.a)
end
return color
end;
["resource"] = function(value)
return resource_types[value.type](value)
end;
["font"] = function(value)
return resource.load_font(value.asset_name)
end;
}
local function parse_config(options, config)
local function parse_recursive(options, config, target)
for _, option in ipairs(options) do
local name = option.name
if name then
if option.type == "list" then
local list = {}
for _, child_config in ipairs(config[name]) do
local child = {}
parse_recursive(option.items, child_config, child)
list[#list + 1] = child
end
target[name] = list
else
target[name] = types[option.type](config[name])
end
end
end
end
local current_config = {}
parse_recursive(options, config, current_config)
return current_config
end
return {
parse_config = parse_config;
}

553
hosted.py Normal file
View file

@ -0,0 +1,553 @@
#
# Part of info-beamer hosted. You can find the latest version
# of this file at:
#
# https://github.com/info-beamer/package-sdk
#
# Copyright (c) 2014,2015,2016,2017,2018 Florian Wesch <fw@info-beamer.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
VERSION = "1.3"
import os
import sys
import json
import time
import errno
import socket
import select
import pyinotify
import thread
import threading
import requests
from tempfile import NamedTemporaryFile
types = {}
def init_types():
def type(fn):
types[fn.__name__] = fn
return fn
@type
def color(value):
return value
@type
def string(value):
return value
@type
def text(value):
return value
@type
def section(value):
return value
@type
def boolean(value):
return value
@type
def select(value):
return value
@type
def duration(value):
return value
@type
def integer(value):
return value
@type
def float(value):
return value
@type
def font(value):
return value
@type
def device(value):
return value
@type
def resource(value):
return value
@type
def json(value):
return value
@type
def custom(value):
return value
@type
def date(value):
return value
init_types()
def log(msg):
print >>sys.stderr, "[hosted.py] %s" % msg
def abort_service(reason):
log("restarting service (%s)" % reason)
try:
thread.interrupt_main()
except:
pass
time.sleep(2)
os.kill(os.getpid(), 2)
time.sleep(2)
os.kill(os.getpid(), 15)
time.sleep(2)
os.kill(os.getpid(), 9)
time.sleep(100)
class Configuration(object):
def __init__(self):
self._restart = False
self._options = []
self._config = {}
self._parsed = {}
self.parse_node_json(do_update=False)
self.parse_config_json()
def restart_on_update(self):
log("going to restart when config is updated")
self._restart = True
def parse_node_json(self, do_update=True):
with open("node.json") as f:
self._options = json.load(f).get('options', [])
if do_update:
self.update_config()
def parse_config_json(self, do_update=True):
with open("config.json") as f:
self._config = json.load(f)
if do_update:
self.update_config()
def update_config(self):
if self._restart:
return abort_service("restart_on_update set")
def parse_recursive(options, config, target):
# print 'parsing', config
for option in options:
if not 'name' in option:
continue
if option['type'] == 'list':
items = []
for item in config[option['name']]:
parsed = {}
parse_recursive(option['items'], item, parsed)
items.append(parsed)
target[option['name']] = items
continue
target[option['name']] = types[option['type']](config[option['name']])
parsed = {}
parse_recursive(self._options, self._config, parsed)
log("updated config")
self._parsed = parsed
@property
def raw(self):
return self._config
@property
def metadata(self):
return self._config['__metadata']
def __getitem__(self, key):
return self._parsed[key]
def __getattr__(self, key):
return self._parsed[key]
def setup_inotify(configuration):
class EventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
basename = os.path.basename(event.pathname)
if basename == 'node.json':
log("node.json changed")
configuration.parse_node_json()
elif basename == 'config.json':
log("config.json changed!")
configuration.parse_config_json()
elif basename.endswith('.py'):
abort_service("python file changed")
wm = pyinotify.WatchManager()
notifier = pyinotify.ThreadedNotifier(wm, EventHandler())
notifier.daemon = True
notifier.start()
wm.add_watch('.', pyinotify.IN_MOVED_TO)
class Node(object):
def __init__(self, node):
self._node = node
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def send_raw(self, raw):
log("sending %r" % (raw,))
self._sock.sendto(raw, ('127.0.0.1', 4444))
def send(self, data):
self.send_raw(self._node + data)
@property
def is_top_level(self):
return self._node == "root"
@property
def path(self):
return self._node
def write_file(self, filename, content):
f = NamedTemporaryFile(prefix='.hosted-py-tmp', dir=os.getcwd())
try:
f.write(content)
except:
traceback.print_exc()
f.close()
raise
else:
f.delete = False
f.close()
os.rename(f.name, filename)
def write_json(self, filename, data):
self.write_file(filename, json.dumps(
data,
ensure_ascii=False,
separators=(',',':'),
).encode('utf8'))
class Sender(object):
def __init__(self, node, path):
self._node = node
self._path = path
def __call__(self, data):
if isinstance(data, (dict, list)):
raw = "%s:%s" % (self._path, json.dumps(
data,
ensure_ascii=False,
separators=(',',':'),
).encode('utf8'))
else:
raw = "%s:%s" % (self._path, data)
self._node.send_raw(raw)
def __getitem__(self, path):
return self.Sender(self, self._node + path)
def __call__(self, data):
return self.Sender(self, self._node)(data)
def scratch_cached(self, filename, generator):
cached = os.path.join(os.environ['SCRATCH'], filename)
if not os.path.exists(cached):
f = NamedTemporaryFile(prefix='scratch-cached-tmp', dir=os.environ['SCRATCH'])
try:
generator(f)
except:
raise
else:
f.delete = False
f.close()
os.rename(f.name, cached)
if os.path.exists(filename):
try:
os.unlink(filename)
except:
pass
os.symlink(cached, filename)
class APIError(Exception):
pass
class APIProxy(object):
def __init__(self, apis, api_name):
self._apis = apis
self._api_name = api_name
@property
def url(self):
index = self._apis.get_api_index()
if not self._api_name in index:
raise APIError("api '%s' not available" % (self._api_name,))
return index[self._api_name]['url']
def unwrap(self, r):
r.raise_for_status()
if r.status_code == 304:
return None
if r.headers['content-type'] == 'application/json':
resp = r.json()
if not resp['ok']:
raise APIError(u"api call failed: %s" % (
resp.get('error', '<unknown error>'),
))
return resp.get(self._api_name)
else:
return r.content
def add_defaults(self, kwargs):
if not 'timeout' in kwargs:
kwargs['timeout'] = 10
def get(self, **kwargs):
self.add_defaults(kwargs)
try:
return self.unwrap(self._apis.session.get(
url = self.url,
**kwargs
))
except APIError:
raise
except Exception as err:
raise APIError(err)
def post(self, **kwargs):
self.add_defaults(kwargs)
try:
return self.unwrap(self._apis.session.post(
url = self.url,
**kwargs
))
except APIError:
raise
except Exception as err:
raise APIError(err)
class APIs(object):
def __init__(self, config):
self._config = config
self._index = None
self._valid_until = 0
self._lock = threading.Lock()
self._session = requests.Session()
self._session.headers.update({
'User-Agent': 'hosted.py version/%s' % (VERSION,)
})
def update_apis(self):
log("fetching api index")
r = self._session.get(
url = self._config.metadata['api'],
timeout = 5,
)
r.raise_for_status()
resp = r.json()
if not resp['ok']:
raise APIError("cannot retrieve api index")
self._index = resp['apis']
self._valid_until = resp['valid_until'] - 300
def get_api_index(self):
with self._lock:
now = time.time()
if now > self._valid_until:
self.update_apis()
return self._index
@property
def session(self):
return self._session
def list(self):
try:
index = self.get_api_index()
return sorted(index.keys())
except Exception as err:
raise APIError(err)
def __getitem__(self, api_name):
return APIProxy(self, api_name)
def __getattr__(self, api_name):
return APIProxy(self, api_name)
class GPIO(object):
def __init__(self):
self._pin_fd = {}
self._state = {}
self._fd_2_pin = {}
self._poll = select.poll()
self._lock = threading.Lock()
def setup_pin(self, pin, direction="in", invert=False):
if not os.path.exists("/sys/class/gpio/gpio%d" % pin):
with open("/sys/class/gpio/export", "wb") as f:
f.write(str(pin))
# mdev is giving the newly create GPIO directory correct permissions.
for i in range(10):
try:
with open("/sys/class/gpio/gpio%d/active_low" % pin, "wb") as f:
f.write("1" if invert else "0")
break
except IOError as err:
if err.errno != errno.EACCES:
raise
time.sleep(0.1)
log("waiting for GPIO permissions")
else:
raise IOError(errno.EACCES, "Cannot access GPIO")
with open("/sys/class/gpio/gpio%d/direction" % pin, "wb") as f:
f.write(direction)
def set_pin_value(self, pin, high):
with open("/sys/class/gpio/gpio%d/value" % pin, "wb") as f:
f.write("1" if high else "0")
def monitor(self, pin, invert=False):
if pin in self._pin_fd:
return
self.setup_pin(pin, direction="in", invert=invert)
with open("/sys/class/gpio/gpio%d/edge" % pin, "wb") as f:
f.write("both")
fd = os.open("/sys/class/gpio/gpio%d/value" % pin, os.O_RDONLY)
self._state[pin] = bool(int(os.read(fd, 5)))
self._fd_2_pin[fd] = pin
self._pin_fd[pin] = fd
self._poll.register(fd, select.POLLPRI | select.POLLERR)
def poll(self, timeout=1000):
changes = []
for fd, evt in self._poll.poll(timeout):
os.lseek(fd, 0, 0)
state = bool(int(os.read(fd, 5)))
pin = self._fd_2_pin[fd]
with self._lock:
prev_state, self._state[pin] = self._state[pin], state
if state != prev_state:
changes.append((pin, state))
return changes
def poll_forever(self):
while 1:
for event in self.poll():
yield event
def on(self, pin):
with self._lock:
return self._state.get(pin, False)
class Device(object):
def __init__(self):
self._socket = None
self._gpio = GPIO()
@property
def gpio(self):
return self._gpio
@property
def screen_resolution(self):
with open("/sys/class/graphics/fb0/virtual_size", "rb") as f:
return [int(val) for val in f.read().strip().split(',')]
def ensure_connected(self):
if self._socket:
return True
try:
log("establishing upstream connection")
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._socket.connect(os.getenv('SYNCER_SOCKET', "/tmp/syncer"))
return True
except Exception as err:
log("cannot connect to upstream socket: %s" % (err,))
return False
def send_raw(self, raw):
try:
if self.ensure_connected():
self._socket.send(raw + '\n')
except Exception as err:
log("cannot send to upstream: %s" % (err,))
if self._socket:
self._socket.close()
self._socket = None
def send_upstream(self, **data):
self.send_raw(json.dumps(data))
def turn_screen_off(self):
self.send_raw("tv off")
def turn_screen_on(self):
self.send_raw("tv on")
def screen(self, on=True):
if on:
self.turn_screen_on()
else:
self.turn_screen_off()
def reboot(self):
self.send_raw("system reboot")
def halt_until_powercycled(self):
self.send_raw("system halt")
def restart_infobeamer(self):
self.send_raw("infobeamer restart")
def verify_cache(self):
self.send_raw("syncer verify_cache")
if __name__ == "__main__":
device = Device()
while 1:
try:
command = raw_input("syncer> ")
device.send_raw(command)
except (KeyboardInterrupt, EOFError):
break
except:
import traceback
traceback.print_exc()
else:
log("starting version %s" % (VERSION,))
node = NODE = Node(os.environ['NODE'])
device = DEVICE = Device()
config = CONFIG = Configuration()
api = API = APIs(CONFIG)
setup_inotify(CONFIG)
log("ready to go!")

45
node.json Normal file
View file

@ -0,0 +1,45 @@
{
"name": "Icinga2 Statusmonitor",
"permissions": {
"network": "Needs to connect to icinga2 api"
},
"options": [{
"title": "Background color",
"ui_width": 4,
"name": "background_color",
"type": "color",
"default": [0,0,0,1]
}, {
"title": "Font",
"ui_width": 8,
"name": "font",
"type": "font",
"default": "silkscreen.ttf"
}, {
"title": "icinga2 API user",
"ui_width": 6,
"name": "api_user",
"type": "string",
"default": "readonly"
}, {
"title": "icinga2 API password",
"ui_width": 6,
"name": "api_password",
"type": "string",
"default": "really_secure"
}, {
"title": "icinga2 API URL for hosts",
"ui_width": 12,
"name": "url_hosts",
"type": "string",
"default": "https://icinga2/api/v1/objects/hosts",
"hint": "Full URL to the API endpoint which returns a list of monitored hosts"
}, {
"title": "icinga2 API URL for services",
"ui_width": 12,
"name": "url_services",
"type": "string",
"default": "https://icinga2/api/v1/objects/services?filter=service.state!=ServiceOK",
"hint": "Full URL to the API endpoint which returns a list of monitored services. Keeping the filter is strongly recommended!"
}]
}

69
node.lua Normal file
View file

@ -0,0 +1,69 @@
util.init_hosted()
local json = require "json"
local services = {}
local host_width = 0
local time_width = 0
local c_hard = {}
c_hard[0] = resource.create_colored_texture(0, 0.6, 0, 1)
c_hard[1] = resource.create_colored_texture(0.8, 0.9, 0, 1)
c_hard[2] = resource.create_colored_texture(0.8, 0, 0, 1)
c_hard[3] = resource.create_colored_texture(0.6, 0, 0.7, 1)
local c_soft = {}
c_soft[1] = resource.create_colored_texture(0.3, 0.4, 0, 1)
c_soft[2] = resource.create_colored_texture(0.3, 0, 0, 1)
c_soft[3] = resource.create_colored_texture(0.3, 0, 0.4, 1)
local c_text = {}
c_text[0] = {1, 1, 1}
c_text[1] = {0, 0, 0}
c_text[2] = {1, 1, 1}
c_text[3] = {1, 1, 1}
gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT)
util.file_watch("services.json", function(content)
services = json.decode(content)
host_width = 0
for idx, service in ipairs(services.services) do
host_width = math.max(host_width, CONFIG.font:width(service.host, 50))
end
time_width = CONFIG.font:width(services.prettytime, 30)
end)
local white = resource.create_colored_texture(1,1,1,1)
local base_time = N.base_time or 0
function node.render()
CONFIG.background_color.clear()
CONFIG.font:write(NATIVE_WIDTH/2-time_width/2, 10, services.prettytime, 30, 1,1,1,1)
local y = 50
for idx, serv in ipairs(services.services) do
my_height = (#serv.output*40)+90
if serv.type == 0 then
c_soft[serv.state]:draw(0, y, NATIVE_WIDTH, y+my_height)
else
c_hard[serv.state]:draw(0, y, NATIVE_WIDTH, y+my_height)
end
y = y+20
CONFIG.font:write(10, y, serv.host, 50, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][2],1)
CONFIG.font:write(host_width+40, y, serv.service, 50, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][3],1)
y = y+60
for idx, line in ipairs(serv.output) do
CONFIG.font:write(host_width+40, y, line, 30, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][3],1)
y = y+40
end
y = y+12
end
end

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"name": "icinga2beamer",
"author": "hi@kunsmann.eu",
"desc": "icinga2 information radiator"
}

BIN
package.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

102
service Executable file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python
import requests
import traceback
import time
import json
import urllib2
import pytz
import hashlib
from datetime import datetime, timedelta
from itertools import islice
from hosted import CONFIG, NODE
CONFIG.restart_on_update()
def current_time():
timezone = pytz.timezone("Europe/Berlin")
now = datetime.utcnow()
now = now.replace(tzinfo=pytz.utc)
now = now.astimezone(timezone)
now = now.replace(tzinfo=None)
return now
def to_unixtimestamp(dt):
return int(time.mktime(dt.timetuple()))
def regenerate():
now = current_time()
services = {
'generated': to_unixtimestamp(now),
'prettytime': now.strftime('%d.%m.%Y %H:%M:%S'),
'services': [],
}
try:
hosts = requests.get(CONFIG["url_hosts"], auth=(CONFIG["api_user"], CONFIG["api_password"]), verify=False).json()
serv = requests.get(CONFIG["url_services"], auth=(CONFIG["api_user"], CONFIG["api_password"]), verify=False).json()
if 'results' not in hosts:
raise KeyError('API call for hosts did not return any results')
if 'results' not in serv:
raise KeyError('API call for services did not return any results')
for host in hosts['results']:
if host['attrs']['downtime_depth'] > 0:
continue
if host['attrs']['problem']:
services['services'].append({
'host': host['attrs']['display_name'],
'service': '-- HOST --',
'state': 2,
'type': host['attrs']['state_type'],
'output': host['attrs']['last_check_result']['output'].splitlines(),
})
for svc in serv['results']:
if host['attrs']['downtime_depth'] > 0:
continue
if svc['attrs']['problem']:
services['services'].append({
'host': svc['attrs']['host_name'],
'service': svc['attrs']['display_name'],
'state': svc['attrs']['state'],
'type': svc['attrs']['state_type'],
'output': svc['attrs']['last_check_result']['output'].splitlines(),
})
services['services'].sort(key=lambda x: str(x['type'])+str(x['state'])+x['host']+x['service'])
services['services'].reverse()
except Exception as e:
services['services'].append({
'host': 'icinga2beamer',
'service': 'INTERNAL',
'state': 2,
'output': [repr(e)],
})
if len(services['services']) == 0:
services['services'].append({
'host': '',
'service': 'icinga2',
'state': 0,
'output': ['Everything is fine. Go get some coffee.'],
})
with file("services.json", "wb") as f:
f.write(json.dumps(services, ensure_ascii=False).encode("utf8"))
def main():
while 1:
try:
regenerate()
except Exception:
traceback.print_exc()
time.sleep(20)
if __name__ == "__main__":
main()

BIN
silkscreen.ttf Normal file

Binary file not shown.