docker: add kodi notifications

This commit is contained in:
Lukas Rusak 2016-05-13 15:35:55 -07:00
parent 189c3afeff
commit 6b55e44eba
6 changed files with 614 additions and 7 deletions

View File

@ -1,3 +1,6 @@
8.0.103
- Allow using kodi notifications based on Docker events API
8.0.102 8.0.102
- Update to docker 1.11.1 - Update to docker 1.11.1

View File

@ -18,7 +18,7 @@
PKG_NAME="docker" PKG_NAME="docker"
PKG_VERSION="1.11.1" PKG_VERSION="1.11.1"
PKG_REV="102" PKG_REV="103"
PKG_ARCH="any" PKG_ARCH="any"
PKG_ADDON_PROJECTS="Generic RPi RPi2" PKG_ADDON_PROJECTS="Generic RPi RPi2"
PKG_LICENSE="ASL" PKG_LICENSE="ASL"

View File

@ -19,13 +19,13 @@
import os import os
import subprocess import subprocess
import sys import sys
import threading
import time import time
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
sys.path.append('/usr/share/kodi/addons/service.libreelec.settings') sys.path.append('/usr/share/kodi/addons/service.libreelec.settings')
import oe import oe
__author__ = 'lrusak' __author__ = 'lrusak'
@ -36,20 +36,257 @@ __servicename__ = __addon__.getAddonInfo('id') + '.service'
__socket__ = __path__ + '/systemd/' + __addon__.getAddonInfo('id') + '.socket' __socket__ = __path__ + '/systemd/' + __addon__.getAddonInfo('id') + '.socket'
__socketname__ = __addon__.getAddonInfo('id') + '.socket' __socketname__ = __addon__.getAddonInfo('id') + '.socket'
sys.path.append(__path__ + '/lib')
import dockermon
# docker events for api 1.23 (docker version 1.11.x)
# https://docs.docker.com/engine/reference/api/docker_remote_api_v1.23/#monitor-docker-s-events
docker_events = {
'container': {
'string': 30030,
'event': {
'attach': {
'string': 30031,
'enabled': '',
},
'commit': {
'string': 30032,
'enabled': '',
},
'copy': {
'string': 30033,
'enabled': '',
},
'create': {
'string': 30034,
'enabled': '',
},
'destroy': {
'string': 30035,
'enabled': '',
},
'die': {
'string': 30036,
'enabled': '',
},
'exec_create': {
'string': 30037,
'enabled': '',
},
'exec_start': {
'string': 30038,
'enabled': '',
},
'export': {
'string': 30039,
'enabled': '',
},
'kill': {
'string': 30040,
'enabled': True,
},
'oom': {
'string': 30041,
'enabled': True,
},
'pause': {
'string': 30042,
'enabled': '',
},
'rename': {
'string': 30043,
'enabled': '',
},
'resize': {
'string': 30044,
'enabled': '',
},
'restart': {
'string': 30045,
'enabled': '',
},
'start': {
'string': 30046,
'enabled': True,
},
'stop': {
'string': 30047,
'enabled': True,
},
'top': {
'string': 30048,
'enabled': '',
},
'unpause': {
'string': 30049,
'enabled': '',
},
'update': {
'string': 30050,
'enabled': '',
},
},
},
'image': {
'string': 30060,
'event': {
'delete': {
'string': 30061,
'enabled': '',
},
'import': {
'string': 30062,
'enabled': '',
},
'pull': {
'string': 30063,
'enabled': True,
},
'push': {
'string': 30064,
'enabled': '',
},
'tag': {
'string': 30065,
'enabled': '',
},
'untag': {
'string': 30066,
'enabled': '',
},
},
},
'volume': {
'string': 30070,
'event': {
'create': {
'string': 30071,
'enabled': '',
},
'mount': {
'string': 30072,
'enabled': '',
},
'unmount': {
'string': 30073,
'enabled': '',
},
'destroy': {
'string': 30074,
'enabled': '',
},
},
},
'network': {
'string': 30080,
'event': {
'create': {
'string': 30081,
'enabled': '',
},
'connect': {
'string': 30082,
'enabled': '',
},
'disconnect': {
'string': 30083,
'enabled': '',
},
'destroy': {
'string': 30084,
'enabled': '',
},
},
},
}
def print_notification(json_data):
event_string = docker_events[json_data['Type']]['event'][json_data['Action']]['string']
if __addon__.getSetting('notifications') is '0': # default
if docker_events[json_data['Type']]['event'][json_data['Action']]['enabled']:
try:
message = unicode(' '.join([__addon__.getLocalizedString(30010),
json_data['Actor']['Attributes']['name'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
except KeyError as e:
message = unicode(' '.join([__addon__.getLocalizedString(30011),
json_data['Type'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
elif __addon__.getSetting('notifications') is '1': # all
try:
message = unicode(' '.join([__addon__.getLocalizedString(30010),
json_data['Actor']['Attributes']['name'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
except KeyError as e:
message = unicode(' '.join([__addon__.getLocalizedString(30011),
json_data['Type'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
elif __addon__.getSetting('notifications') is '2': # none
pass
elif __addon__.getSetting('notifications') is '3': # custom
if __addon__.getSetting(json_data['Action']) == 'true':
try:
message = unicode(' '.join([__addon__.getLocalizedString(30010),
json_data['Actor']['Attributes']['name'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
except KeyError as e:
message = unicode(' '.join([__addon__.getLocalizedString(30011),
json_data['Type'],
'|',
__addon__.getLocalizedString(30012),
__addon__.getLocalizedString(event_string)]))
dialog = xbmcgui.Dialog()
try:
if message is not '':
length = int(__addon__.getSetting('notification_length')) * 1000
dialog.notification('Docker', message, '/storage/.kodi/addons/service.system.docker/icon.png', length)
xbmc.log('## service.system.docker ## ' + unicode(message))
except NameError as e:
pass
class dockermonThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self._is_running = True
def run(self):
while self._is_running:
dockermon.watch(print_notification)
def stop(self):
self._is_running = False
class Main(object): class Main(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
monitor = DockerMonitor(self) monitor = DockerMonitor(self)
if not Docker().is_active():
if not Docker().is_enabled(): if not Docker().is_enabled():
Docker().enable() Docker().enable()
Docker().start() Docker().start()
while not monitor.abortRequested(): while not monitor.abortRequested():
if monitor.waitForAbort(): if monitor.waitForAbort():
Docker().stop() # we don't want to stop or disable docker while it's installed
Docker().disable() pass
class Docker(object): class Docker(object):
@ -95,9 +332,11 @@ class DockerMonitor(xbmc.Monitor):
xbmc.Monitor.__init__(self) xbmc.Monitor.__init__(self)
def onSettingsChanged(self): def onSettingsChanged(self):
Docker().restart() pass
if ( __name__ == "__main__" ): if ( __name__ == "__main__" ):
dockermonThread().start()
Main() Main()
del DockerMonitor del DockerMonitor
dockermonThread().stop()

View File

@ -0,0 +1,153 @@
#!/usr/bin/env python
"""docker monitor using docker /events HTTP streaming API"""
"""https://github.com/CyberInt/dockermon"""
from contextlib import closing
from functools import partial
from socket import socket, AF_UNIX
from subprocess import Popen, PIPE
from sys import stdout, version_info
import json
import shlex
if version_info[:2] < (3, 0):
from httplib import OK as HTTP_OK
from urlparse import urlparse
else:
from http.client import OK as HTTP_OK
from urllib.parse import urlparse
__version__ = '0.2.2'
# buffer size must be 256 or lower otherwise events won't show in realtime
bufsize = 256
default_sock_url = 'ipc:///var/run/docker.sock'
class DockermonError(Exception):
pass
def read_http_header(sock):
"""Read HTTP header from socket, return header and rest of data."""
buf = []
hdr_end = '\r\n\r\n'
while True:
buf.append(sock.recv(bufsize).decode('utf-8'))
data = ''.join(buf)
i = data.find(hdr_end)
if i == -1:
continue
return data[:i], data[i + len(hdr_end):]
def header_status(header):
"""Parse HTTP status line, return status (int) and reason."""
status_line = header[:header.find('\r')]
# 'HTTP/1.1 200 OK' -> (200, 'OK')
fields = status_line.split(None, 2)
return int(fields[1]), fields[2]
def connect(url):
"""Connect to UNIX or TCP socket.
url can be either tcp://<host>:port or ipc://<path>
"""
url = urlparse(url)
if url.scheme == 'tcp':
sock = socket()
netloc = tuple(url.netloc.rsplit(':', 1))
hostname = socket.gethostname()
elif url.scheme == 'ipc':
sock = socket(AF_UNIX)
netloc = url.path
hostname = 'localhost'
else:
raise ValueError('unknown socket type: %s' % url.scheme)
sock.connect(netloc)
return sock, hostname
def watch(callback, url=default_sock_url):
"""Watch docker events. Will call callback with each new event (dict).
url can be either tcp://<host>:port or ipc://<path>
"""
sock, hostname = connect(url)
request = 'GET /events HTTP/1.1\nHost: %s\n\n' % hostname
request = request.encode('utf-8')
with closing(sock):
sock.sendall(request)
header, payload = read_http_header(sock)
status, reason = header_status(header)
if status != HTTP_OK:
raise DockermonError('bad HTTP status: %s %s' % (status, reason))
# Messages are \r\n<size in hex><JSON payload>\r\n
buf = [payload]
while True:
chunk = sock.recv(bufsize)
if not chunk:
raise EOFError('socket closed')
buf.append(chunk.decode('utf-8'))
data = ''.join(buf)
i = data.find('\r\n')
if i == -1:
continue
size = int(data[:i], 16)
start = i + 2 # Skip initial \r\n
if len(data) < start + size + 2:
continue
payload = data[start:start+size]
callback(json.loads(payload))
buf = [data[start+size+2:]] # Skip \r\n suffix
def print_callback(msg):
"""Print callback, prints message to stdout as JSON in one line."""
json.dump(msg, stdout)
stdout.write('\n')
stdout.flush()
def prog_callback(prog, msg):
"""Program callback, calls prog with message in stdin"""
pipe = Popen(prog, stdin=PIPE)
data = json.dumps(msg)
pipe.stdin.write(data.encode('utf-8'))
pipe.stdin.close()
if __name__ == '__main__':
from argparse import ArgumentParser
parser = ArgumentParser(description=__doc__)
parser.add_argument('--prog', default=None,
help='program to call (e.g. "jq --unbuffered .")')
parser.add_argument(
'--socket-url', default=default_sock_url,
help='socket url (ipc:///path/to/sock or tcp:///host:port)')
parser.add_argument(
'--version', help='print version and exit',
action='store_true', default=False)
args = parser.parse_args()
if args.version:
print('dockermon %s' % __version__)
raise SystemExit
if args.prog:
prog = shlex.split(args.prog)
callback = partial(prog_callback, prog)
else:
callback = print_callback
try:
watch(callback, args.socket_url)
except (KeyboardInterrupt, EOFError):
pass

View File

@ -0,0 +1,197 @@
# Kodi Media Center language file
# Addon Name: docker
# Addon id: service.system.docker
msgid ""
msgstr ""
msgctxt "#30000"
msgid "Settings"
msgstr ""
msgctxt "#30001"
msgid "Notifications"
msgstr ""
msgctxt "#30002"
msgid "Default"
msgstr ""
msgctxt "#30003"
msgid "All"
msgstr ""
msgctxt "#30004"
msgid "Off"
msgstr ""
msgctxt "#30005"
msgid "Custom"
msgstr ""
msgctxt "#30006"
msgid "Notification Length (Seconds)"
msgstr ""
msgctxt "#30010"
msgid "Name:"
msgstr ""
msgctxt "#30011"
msgid "Type:"
msgstr ""
msgctxt "#30012"
msgid "Action:"
msgstr ""
msgctxt "#30030"
msgid "Container"
msgstr ""
msgctxt "#30031"
msgid "attach"
msgstr ""
msgctxt "#30032"
msgid "commit"
msgstr ""
msgctxt "#30033"
msgid "copy"
msgstr ""
msgctxt "#30034"
msgid "create"
msgstr ""
msgctxt "#30035"
msgid "destroy"
msgstr ""
msgctxt "#30036"
msgid "die"
msgstr ""
msgctxt "#30037"
msgid "exec_create"
msgstr ""
msgctxt "#30038"
msgid "exec_start"
msgstr ""
msgctxt "#30039"
msgid "export"
msgstr ""
msgctxt "#30040"
msgid "kill"
msgstr ""
msgctxt "#30041"
msgid "out of memory"
msgstr ""
msgctxt "#30042"
msgid "pause"
msgstr ""
msgctxt "#30043"
msgid "rename"
msgstr ""
msgctxt "#30044"
msgid "resize"
msgstr ""
msgctxt "#30045"
msgid "restart"
msgstr ""
msgctxt "#30046"
msgid "start"
msgstr ""
msgctxt "#30047"
msgid "stop"
msgstr ""
msgctxt "#30048"
msgid "top"
msgstr ""
msgctxt "#30049"
msgid "unpause"
msgstr ""
msgctxt "#30050"
msgid "update"
msgstr ""
msgctxt "#30060"
msgid "Image"
msgstr ""
msgctxt "#30061"
msgid "delete"
msgstr ""
msgctxt "#30062"
msgid "import"
msgstr ""
msgctxt "#30063"
msgid "pull"
msgstr ""
msgctxt "#30064"
msgid "push"
msgstr ""
msgctxt "#30065"
msgid "tag"
msgstr ""
msgctxt "#30066"
msgid "untag"
msgstr ""
msgctxt "#30070"
msgid "Volume"
msgstr ""
msgctxt "#30071"
msgid "create"
msgstr ""
msgctxt "#30072"
msgid "mount"
msgstr ""
msgctxt "#30073"
msgid "unmount"
msgstr ""
msgctxt "#30074"
msgid "destroy"
msgstr ""
msgctxt "#30080"
msgid "Network"
msgstr ""
msgctxt "#30081"
msgid "create"
msgstr ""
msgctxt "#30082"
msgid "connect"
msgstr ""
msgctxt "#30083"
msgid "disconnect"
msgstr ""
msgctxt "#30084"
msgid "destroy"
msgstr ""

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<category label="30000">
<setting label="30001" type="enum" id="notifications" default="0" lvalues="30002|30003|30004|30005"/>
<setting label="30034" type="bool" id="create" default="false" visible="eq(-1,3)" subsetting="true"/>
<setting label="30035" type="bool" id="destroy" default="false" visible="eq(-2,3)" subsetting="true"/>
<setting label="30036" type="bool" id="die" default="false" visible="eq(-3,3)" subsetting="true"/>
<setting label="30040" type="bool" id="kill" default="false" visible="eq(-4,3)" subsetting="true"/>
<setting label="30041" type="bool" id="oom" default="false" visible="eq(-5,3)" subsetting="true"/>
<setting label="30046" type="bool" id="start" default="false" visible="eq(-6,3)" subsetting="true"/>
<setting label="30047" type="bool" id="stop" default="false" visible="eq(-7,3)" subsetting="true"/>
<setting label="30006" type="slider" id="notification_length" default="5" visible="lt(-8,2) | gt(-8,2)" range="2,10" option="int" />
</category>
</settings>