diff --git a/checkzfs.py b/checkzfs.py deleted file mode 100644 index 12d8cd5..0000000 --- a/checkzfs.py +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env python3 -# vim:fenc=utf-8:noet -## Copyright 2021 sysops.tv ;-) -## BSD-2-Clause -## -## Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -## -## 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -## -## 2. 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.38 - -import sys -import re -import subprocess -import time -import json -import os.path -import socket -from email.message import EmailMessage - -class zfscheck(object): - ZFSLIST_REGEX = re.compile("^(?P.*?)(?:|@(?P.*?))\t(?P\d+)\t(?P\d+)\t(?P\d+)\t(?P\d+)$",re.M) ## todo used/referenced ... diff zum replica - ZFS_LOCAL_SNAPSHOTS = [] - ZFS_DATASTORES = {} - ZFS_RESULT_SNAPSHOT = {} - VALIDCOLUMNS = ["dataset","snapshot","creation","guid","used","referenced","size","age","status","copy"] ## valid columns - DEFAULT_COLUMNS = ["dataset","snapshot","age","count","copy"] ## default columns - DATEFORMAT = "%a %d.%b.%Y %H:%M" - COLOR_CONSOLE = { - "warn" : "\033[93m", - "crit" : "\033[91m", - "reset" : "\033[0m" - } - COLUMN_NAMES = { ## Namen frei editierbar - "dataset" : "Dataset", - "snapshot" : "Snapshotname", - "creation" : "Erstellungszeit", - "age" : "Alter", - "count" : "Anzahl", - "used" : "Größe", - "copy" : "Replikat" - } - COLUMN_ALIGN = { - "dataset" : "<", - "snapshot" : "<", - "copy" : "<", - "status" : "^" - } - - TIME_MULTIPLICATOR = { ## todo - "h" : 60, ## Stunden - "d" : 60*24, ## Tage - "w" : 60 * 24 * 7, ## Wochen - "m" : 60 * 24 * 30 ## Monat - } - COLUMN_MAPPER = {} - - def __init__(self,**kwargs): - _start_time = time.time() - self.remote = None - self.filter = None - self.rawdata = False - self.sortreverse = False - self._check_kwargs(kwargs) - _data = self.get_data() - _script_runtime = time.time() - _start_time - if self.output == "text": - print(self.table_output(_data)) - if self.output == "html": - print( self.html_output(_data) ) - if self.output == "mail": - self.mail_output(_data) - if self.output == "checkmk": - self.checkmk_output(_data) - if self.output == "json": - print(self.json_output(_data)) - - print ("Runtime: {0:.2f}".format(_script_runtime)) - - def _check_kwargs(self,kwargs): - ## argumente überprüfen - for _k,_v in kwargs.items(): - if _k == "columns": - _default = self.DEFAULT_COLUMNS[:] - - if not _v: - self.columns = _default - continue ## defaults - # add modus wenn mit + - if not _v.startswith("+"): - _default = [] - else: - _v = _v[1:] - _v = _v.split(",") - for _column in _v: - if _column not in self.VALIDCOLUMNS: - raise Exception("invalid column {0} ({1})".format(_v,",".join(self.VALIDCOLUMNS))) - _default.append(_column) - _v = list(_default) - - - if _k == "sort" and _v: - ## sortierung desc wenn mit + - if _v.startswith("+"): - self.sortreverse = True - _v = _v[1:] - if _v not in self.VALIDCOLUMNS: - raise Exception("invalid sort column: {0} ({1})".format(_v,",".join(self.VALIDCOLUMNS))) - - if _k == "threshold" and _v: - _v = _v.split(",") - ## todo tage etc - _v = list(map(int,_v[:2])) ## convert zu int - if len(_v) == 1: - _v = (float("inf"),_v[0]) - - if _k == "filter" and _v: - _v = re.compile(_v) - - setattr(self,_k,_v) - - ## funktionen zum anzeigen / muss hier da sonst kein self - if not self.rawdata: - self.COLUMN_MAPPER = { - "creation" : self.convert_ts_date, - "age" : self.seconds2timespan, - "used" : self.format_bytes, - "size" : self.format_bytes, - "referenced" : self.format_bytes, - } - - def get_data(self): - _data = self._call_proc() - self.get_local_snapshots(_data) - if self.remote: - _data = self._call_proc(self.remote) - - return self.get_snapshot_results(_data) - - def get_local_snapshots(self,data): - for _entry in self._parse(data): - _entry.update({ - "creation" : int(_entry.get("creation",0)) - }) - if _entry.get("snapshot") == None: - self.ZFS_DATASTORES[_entry.get("dataset")] = _entry - self.ZFS_LOCAL_SNAPSHOTS.append(_entry) - - def get_snapshot_results(self,data): - _now = time.time() - for _entry in self._parse(data): - if _entry.get("snapshot") == None: - continue ## TODO - if self.filter and not self.filter.search("{dataset}@{snapshot}".format(**_entry)): - continue - _timestamp = int(_entry.get("creation",0)) - _dataset = _entry["dataset"] - _entry.update({ - "creation" : _timestamp, - "age" : int(_now - _timestamp), - "count" : 1, - "size" : self.ZFS_DATASTORES.get(_dataset,{}).get("used",0), - "used" : int(_entry.get("used",0)), - "referenced": int(_entry.get("referenced",0)), - "copy" : "", - "status" : self.check_threshold(_now -_timestamp) - }) - _copys = list( - filter(lambda x: x.get("guid") == _entry.get("guid") and x.get("dataset") != _entry.get("dataset") , self.ZFS_LOCAL_SNAPSHOTS) - ) - if len(_copys) > 0: - _entry["copy"] = ",".join(["{0}".format(_x.get("dataset")) for _x in _copys]) - else: - if self.backup: - continue - _exist_entry = self.ZFS_RESULT_SNAPSHOT.get(_dataset) - if _exist_entry: - _entry["count"] += _exist_entry.get("count") ## update counter - if _exist_entry.get("creation") <= _entry.get("creation"): ## newer - _exist_entry.update(_entry) - else: - _exist_entry["count"] = _entry["count"] - else: - self.ZFS_RESULT_SNAPSHOT[_dataset] = _entry - - - return list(self.ZFS_RESULT_SNAPSHOT.values()) - - def _parse(self,data): - _ret = [] ## Fixme - for _match in self.ZFSLIST_REGEX.finditer(data): - #yield _match.groupdict() - _ret.append(_match.groupdict()) - return _ret - - def _call_proc(self,remote=None): - zfs_args = ["zfs", "list", - "-t", "all", ## list snapshots / TODO:all - "-Hp", ## script und numeric output - "-o", "name,creation,guid,used,referenced", ## attributes to show - "-r" ## recursive - ] - if remote: - _privkeyoption = [] - if self.ssh_identity: - _privkeyoption = ["-i",self.ssh_identity] - _sshoptions = "BatchMode yes" - _parts = remote.split(":") - _port = "22" ## default port - if len(_parts) > 1: - remote = _parts[0] - _port = _parts[1] - zfs_args = ["ssh", - remote, ## Hostname - "-p", _port, - "-o", _sshoptions, ## ssh options - ] + _privkeyoption + zfs_args - _proc = subprocess.Popen(zfs_args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=False) - _stdout, _stderr = _proc.communicate() - if _proc.returncode > 0: - raise Exception(_stderr.decode(sys.stdout.encoding)) ## Raise Errorlevel with Error from proc - return _stdout.decode(sys.stdout.encoding) - - def convert_ts_date(self,ts): - return time.strftime(self.DATEFORMAT,time.localtime(ts)) - - def check_threshold(self,age): - if not self.threshold: - return "ok" - age /= 60 ## default in minuten - #print("Age: {0} - {1} - {2}".format(age,list(zip(self.threshold,("warn","crit"))),list(filter(lambda y: y[0] < age,zip(self.threshold,("warn","crit")))))) - _status = list( - map(lambda x: x[1], ## return only last - filter(lambda y: y[0] < age, ## check threshold Texte - zip(self.threshold,("warn","crit")) - ) - ) - ) - if not _status: - _status = ["ok"] - return _status[-1] - - - @staticmethod - def format_bytes(size,unit='B'): - # 2**10 = 1024 - size = float(size) - if size == 0: - return "0" - power = 2**10 - n = 0 - power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} - while size > power: - size /= power - n += 1 - return "{0:.2f} {1}{2}".format(size, power_labels[n],unit) - - @staticmethod - def seconds2timespan(seconds,details=2,seperator=" ",template="{0:.0f}{1}",fixedview=False): - _periods = ( - ('W', 604800), - ('T', 86400), - ('Std', 3600), - ('Min', 60), - ('Sek', 1) - ) - _ret = [] - for _name, _period in _periods: - _val = seconds//_period - if _val: - seconds -= _val * _period - #if _val == 1: - # _name = _name[:-1] - _ret.append(template.format(_val,_name)) - else: - if fixedview: - _ret.append("") - return seperator.join(_ret[:details]) - - def _datasort(self,data): - if not self.sort: - return data - return sorted(data, key=lambda k: k[self.sort],reverse=self.sortreverse) - - def checkmk_output(self,data): - if not data: - return - print ("<<>>") - for _item in data: - print("<<{0}>>".format(_item.get("dataset","").replace(" ","_"))) - for _info,_val in _item.items(): - print("{0}: {1}".format(_info,_val)) - - def table_output(self,data): - if not data: - return - _header = data[0].keys() if not self.columns else self.columns - _header_names = [self.COLUMN_NAMES.get(i,i) for i in _header] - _converter = dict((i,self.COLUMN_MAPPER.get(i,(lambda x: str(x)))) for i in _header) - - _output_data = [_header_names] - _line_status = [] - for _item in self._datasort(data): - _line_status.append(_item.get("status")) - _output_data.append([_converter.get(_col)(_item.get(_col,"")) for _col in _header]) - - _maxwidth = [max(map(len,_col)) for _col in zip(*_output_data)] ## max column breite - _format = " ║ ".join(["{{:{}{}}}".format(self.COLUMN_ALIGN.get(_h,">"),_w) for _h,_w in zip(_header,_maxwidth)]) ## format bilden - _line_print = False - _out = [] - _status = "ok" - for _item in _output_data: - if _line_print: - _status = _line_status.pop(0) - if _status != "ok": - _out.append(self.COLOR_CONSOLE.get(_status,"") + _format.format(*_item) + self.COLOR_CONSOLE.get("reset")) - else: - _out.append(_format.format(*_item)) - if not _line_print: - _out.append("═╬═".join(map(lambda x: x*"═",_maxwidth))) ## trennlinie - _line_print = True - return "\n".join(_out) - - def html_output(self,data): - if not data: - return - _header = data[0].keys() if not self.columns else self.columns - _header_names = [self.COLUMN_NAMES.get(i,i) for i in _header] - _converter = dict((i,self.COLUMN_MAPPER.get(i,(lambda x: str(x)))) for i in _header) - - _out = [] - _out.append("") - _out.append("") - _out.append("ZFS") - _out.append("") - _out.append("".format("".format("
{0}
".join(_header_names))) - for _item in self._datasort(data): - _out.append("
{0}
".join([_converter.get(_col)(_item.get(_col,"")) for _col in _header]),_item.get("status","ok"))) - _out.append("
") - return "".join(_out) - - def mail_output(self,data): - _msg = EmailMessage() - self.hostname = socket.getfqdn() - _msg.set_content(self.checkmk_output) - _msg.add_alternative(self.html_output(data),subtype="html") - _msg["From"] = "ZFS-Checkscript on {0}