From 322612761dbb570a332ef7b98182943857345b5c Mon Sep 17 00:00:00 2001 From: Ross Mountjoy Date: Sat, 30 May 2020 08:59:02 -0400 Subject: [PATCH] (MANUALLY ADDING PR FROM Thlb 0.6) Add Docker platform and template app --- dashmachine/platform/docker.py | 394 ++++++++++++++++++++++ dashmachine/static/images/apps/docker.png | Bin 0 -> 21261 bytes template_apps/Docker.ini | 7 + 3 files changed, 401 insertions(+) create mode 100644 dashmachine/platform/docker.py create mode 100644 dashmachine/static/images/apps/docker.png create mode 100644 template_apps/Docker.ini diff --git a/dashmachine/platform/docker.py b/dashmachine/platform/docker.py new file mode 100644 index 0000000..4e3dfd1 --- /dev/null +++ b/dashmachine/platform/docker.py @@ -0,0 +1,394 @@ +""" +##### Docker +Display information from Docker API. Informations can be displayed on a custom card or on an app card (e.g. Portainer App) +```ini +[variable_name] +platform = docker +prefix = http:// +host = localhost +port = 2375 +value_template = {{ value_template }} +``` +> **Returns:** `value_template` as rendered string +| Variable | Required | Description | Options | +|-----------------|----------|-----------------------------------------------------------------|-------------------| +| [variable_name] | Yes | Name for the data source. | [variable_name] | +| platform | Yes | Name of the platform. | docker | +| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// | +| host | Yes | Docker Host | url,ip | +| port | No | Docker Port | port, usually 2375 (Insecure) or 2376 (TLS) | +| api_version | No | Docker API version to use (Default : platform will try to find latest version) | 1.40 | +| tls_mode | No | TLS verification mode, default is None | Server, Client, Both, None | +| tls_ca | No | Requierd for tls_mode=Both or tls_mode=Server, default is None | /path/to/ca, None | +| tls_cert | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/cert, None | +| tls_key | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/key, None| +| card_type | No | Set to Custom if you want to display informations in a custom card. Default is App | Custom, App| +| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template | +
+###### **Available fields for value_template** +* version +* max_api_version +* name +* containers +* containers_running +* containers_paused +* containers_stopped +* images +* driver +* cpu +* memory +* warnings +* error (for debug) +> **Working example (using un-encrypted connection, on Portainer card):** +>```ini +> [docker-endpoint-1] +> platform = docker +> prefix = http:// +> host = 192.168.0.110 +> port = 2375 +> value_template = {{error}}

{{name}}
fiber_manual_record{{containers_running}}fiber_manual_record{{containers_paused}}fiber_manual_record{{containers_stopped}}

+> +> [Portainer] +> prefix = http:// +> url = 192.168.0.110:2375 +> icon = static/images/apps/portainer.png +> sidebar_icon = static/images/apps/portainer.png +> description = Making Docker management easy +> open_in = this_tab +> data_sources = docker-endpoint-1 +>``` +> +> +> **Working example (using encrypted connection, on Portainer card):** +>```ini +> [docker-endpoint-2] +> platform = docker +> prefix = https:// +> host = 192.168.0.110 +> port = 2376 +> tls_mode = Both +> tls_ca = /path/to/ca_file +> tls_cert = /path/to/cert_file +> tls_key = /path/to/key_file +> value_template = {{error}}

{{name}}
fiber_manual_record{{containers_running}}fiber_manual_record{{containers_paused}}fiber_manual_record{{containers_stopped}}

+> +> [Portainer] +> prefix = http:// +> url = 192.168.0.110:2375 +> icon = static/images/apps/portainer.png +> sidebar_icon = static/images/apps/portainer.png +> description = Making Docker management easy +> open_in = this_tab +> data_sources = docker-endpoint-2 +>``` +> +> +> **Working example (using un-encrypted connection, on custom Docker card):** +>```ini +> [docker-endpoint-3] +> platform = docker +> prefix = http:// +> host = 192.168.0.110 +> port = 2375 +> card_type = Custom +> +> [Docker] +> type = custom +> data_sources = docker-endpoint-3 +>``` +""" + +import json +from flask import render_template_string +import requests +import re + + +class Docker(object): + def __init__( + self, + method, + prefix, + host, + port, + api_version, + card_type, + tls_mode, + tls_ca, + tls_cert, + tls_key, + ): + self.endpoint = None + self.method = method + self.prefix = prefix + self.host = host + self.port = port + self.api_version = api_version + self.card_type = card_type + self.tls_mode = tls_mode + self.tls_ca = tls_ca + self.tls_key = tls_key + self.tls_cert = tls_cert + + # Initialize results + self.error = None + self.version = "?" + self.max_api_version = "?" + self.name = "?" + self.running = 0 + self.paused = 0 + self.stopped = 0 + self.images = 0 + self.driver = "?" + self.cpu = "?" + self.memory = "?" + self.html_template = "" + + def check(self): + port = "" if self.port == None else ":" + self.port + + if self.method.upper() == "GET": + try: + response = "" + request = requests.get( + self.prefix + self.host + port + "/v999/info", + verify=self.tls_ca, + cert=(self.tls_cert, self.tls_key), + timeout=10, + ) + response = request.text + if "text/plain" in request.headers["content-type"]: + self.error = request.text + rawdata = None + elif "application/json" in request.headers["content-type"]: + rawdata = request.json() + else: + error = request + rawdata = None + + except Exception as e: + rawdata = None + self.error = f"{e}" + " " + response + self.setHtml() + + if rawdata != None: + if "message" in rawdata: + regex = r"\bv?[0-9]+\.[0-9]+(?:\.[0-9]+)?\b" + r = re.search(regex, rawdata["message"]) + self.max_api_version = r.group(0) + self.api_version = ( + self.api_version + if self.api_version != None + else self.max_api_version + ) + self.endpoint = "/v" + self.api_version + "/" + + def getStatus(self): + port = "" if self.port == None else ":" + self.port + + if self.method.upper() == "GET": + try: + rawdata = requests.get( + self.prefix + self.host + port + self.endpoint + "/info", + verify=self.tls_ca, + cert=(self.tls_cert, self.tls_key), + timeout=10, + ).json() + except Exception as e: + rawdata = None + self.error = f"{e}" + self.setHtml() + + if rawdata != None: + self.name = rawdata["Name"] + self.containers = rawdata["Containers"] + self.containers_running = rawdata["ContainersRunning"] + self.containers_paused = rawdata["ContainersPaused"] + self.containers_stopped = rawdata["ContainersStopped"] + self.images = rawdata["Images"] + self.warnings = rawdata["Warnings"] + self.driver = rawdata["Driver"] + self.cpu = rawdata["NCPU"] + self.memory = self.formatSize(rawdata["MemTotal"]) + if self.card_type == "Custom": + self.setHtml() + + def formatSize(self, size): + # 2**10 = 1024 + power = 2 ** 10 + n = 0 + power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"} + while size > power: + size /= power + n += 1 + return str(round(size, 1)) + " " + power_labels[n] + + def refresh(self): + self.check() + if self.error == None: + self.error = "" + self.getStatus() + + def setHtml(self): + if self.error != None and self.error != "": + self.html_template = """ +
+
+ error +
+
+ Docker +
+
+
+
Error
+
+
+ keyboard_arrow_down +
+
+
+
+
{{ error }}
+
+
+
+ """ + else: + if self.tls_mode == None: + img_tls = """ + lock_open + """ + else: + img_tls = """ + lock + """ + if len(self.warnings) > 0: + img_warnings = """ + warning + """ + else: + img_warnings = """ + warning + """ + self.html_template = ( + """ +
+
+ """ + + img_tls + + img_warnings + + """ +
+
+ Docker +
+
+
+
{{name}}
+
+
+ keyboard_arrow_down +
+
+
+
+
Containers: {{ containers }}
+
Running: {{ containers_running }}
+
Paused: {{ containers_paused }}
+
Stopped: {{ containers_stopped }}
+
Images: {{ images }}
+
Driver: {{ driver }}
+
CPU: {{ cpu }}
+
Memory: {{ memory }}
+
+
+
+ """ + ) + + def getHtml(self): + return self.html_template + + +class Platform: + def __init__(self, *args, **kwargs): + # parse the user's options from the config entries + for key, value in kwargs.items(): + self.__dict__[key] = value + + # set defaults for omitted options + if not hasattr(self, "method"): + self.method = "GET" + if not hasattr(self, "prefix"): + self.prefix = "http://" + if not hasattr(self, "host"): + self.host = None + if not hasattr(self, "port"): + self.port = 2375 + if not hasattr(self, "api_version"): + self.api_version = None + if not hasattr(self, "card_type"): + self.card_type = "App" + if not hasattr(self, "tls_ca"): + self.tls_ca = None + if not hasattr(self, "tls_cert"): + self.tls_cert = None + if not hasattr(self, "tls_key"): + self.tls_key = None + # Without TLS + if not hasattr(self, "tls_mode"): + self.tls_mode = None + self.tls_ca = None + self.tls_cert = None + self.tls_key = None + else: + if self.tls_mode == "Both": + if self.tls_ca == None or self.tls_cert == None or self.tls_key == None: + return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key" + elif self.tls_mode == "Client": + self.tls_ca = False + elif self.tls_mode == "Server": + self.tls_cert = "" + self.tls_key = "" + elif self.tls_mode == "None": + self.tls_ca = None + self.tls_cert = None + self.tls_key = None + + self.docker = Docker( + self.method, + self.prefix, + self.host, + self.port, + self.api_version, + self.card_type, + self.tls_mode, + self.tls_ca, + self.tls_cert, + self.tls_key, + ) + + def process(self): + if self.host == None: + return "host missing" + # TLS check + if self.tls_mode == "Both": + if self.tls_ca == None or self.tls_cert == None or self.tls_key == None: + return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key" + elif self.tls_mode == "Client": + if self.tls_cert == None or self.tls_key == None: + return "tls_mode set to Client, and missing tls_cert/tls_key" + elif self.tls_mode == "Server": + if self.tls_ca == None: + return "tls_mode set to Server, and missing tls_ca" + else: + if self.tls_mode != None: + return "Invalid tls_mode : " + self.tls_mode + + self.docker.refresh() + + if self.card_type == "Custom": + return render_template_string(self.docker.getHtml(), **self.docker.__dict__) + else: + return render_template_string(self.value_template, **self.docker.__dict__) diff --git a/dashmachine/static/images/apps/docker.png b/dashmachine/static/images/apps/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..9b23d4d983ff31bdd94f9f89cf709ef17f13b490 GIT binary patch literal 21261 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 JG@kOy{11lU-#GvP literal 0 HcmV?d00001 diff --git a/template_apps/Docker.ini b/template_apps/Docker.ini new file mode 100644 index 0000000..de77274 --- /dev/null +++ b/template_apps/Docker.ini @@ -0,0 +1,7 @@ +[Docker] +prefix = http:// +url = your-website.com +icon = static/images/apps/docker.png +sidebar_icon = static/images/apps/docker.png +description = Empowering App Development for Developers +open_in = this_tab \ No newline at end of file