init first version of pbs_special_agent
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import itertools
|
||||
import json
|
||||
from datetime import datetime
|
||||
from cmk.agent_based.v2 import (
|
||||
AgentSection,
|
||||
CheckPlugin,
|
||||
Service,
|
||||
Result,
|
||||
State,
|
||||
Metric,
|
||||
get_value_store,
|
||||
)
|
||||
from cmk.plugins.lib.df import (
|
||||
df_check_filesystem_single,
|
||||
FILESYSTEM_DEFAULT_LEVELS,
|
||||
)
|
||||
|
||||
ITEM_STATUS = "API Status"
|
||||
ITEM_DS_USAGE = "Datastore Usage "
|
||||
ITEM_SYNC_JOBS = "Sync Job "
|
||||
ITEM_GC = "Garbage Collection "
|
||||
ITEM_PRUNE = "Prune Job "
|
||||
ITEM_VERIFY = "Verify Job "
|
||||
ITEM_FS_ROOT = "HD Space (root)"
|
||||
ITEM_FS_SWAP = "Swap Usage"
|
||||
ITEM_CPU = "CPU Usage"
|
||||
ITEM_MEM = "Memory Usage"
|
||||
ITEM_LOAD = "Load Average"
|
||||
|
||||
def parse_jobs(item: str, item_key: str, section: dict, section_key: str, section_item_key: str):
|
||||
if item.startswith(item_key):
|
||||
for s in section[section_key]:
|
||||
if item.replace(item_key, '') == s[section_item_key]:
|
||||
next_run = ""
|
||||
if "last-run-state" in s:
|
||||
if 'next-run' in s:
|
||||
next_run = (
|
||||
f", next run: "
|
||||
f"{datetime.fromtimestamp(s['next-run'])}"
|
||||
)
|
||||
if s["last-run-state"] == "OK":
|
||||
yield Result(
|
||||
state=State.OK,
|
||||
summary=(
|
||||
f"last run state: OK"
|
||||
f"{next_run}"
|
||||
),
|
||||
)
|
||||
else:
|
||||
yield Result(
|
||||
state=State.CRIT,
|
||||
summary=(
|
||||
f"last run state: {s["last-run-state"]}"
|
||||
f"{next_run}"
|
||||
),
|
||||
)
|
||||
else:
|
||||
yield Result(
|
||||
state=State.OK,
|
||||
summary=(
|
||||
"Job running"
|
||||
),
|
||||
)
|
||||
|
||||
def parse_pbs_special_agent(string_table):
|
||||
flatlist = list(itertools.chain.from_iterable(string_table))
|
||||
# parsed = json.loads(" ".join(flatlist).replace("'", "\""))
|
||||
parsed = json.loads(" ".join(flatlist))
|
||||
return parsed
|
||||
|
||||
|
||||
def discover_pbs_special_agent(section):
|
||||
yield Service(item=ITEM_STATUS)
|
||||
if "status" in section:
|
||||
yield Service(item=ITEM_CPU)
|
||||
yield Service(item=ITEM_MEM)
|
||||
yield Service(item=ITEM_LOAD)
|
||||
if "sync" in section:
|
||||
for s in section["sync"]:
|
||||
yield Service(item=ITEM_SYNC_JOBS + s["id"])
|
||||
if "gc" in section:
|
||||
for gc in section["gc"]:
|
||||
yield Service(item=ITEM_GC + gc["store"])
|
||||
if "prune" in section:
|
||||
for prune in section["prune"]:
|
||||
yield Service(item=ITEM_PRUNE + prune["id"])
|
||||
if "verify" in section:
|
||||
for verify in section["verify"]:
|
||||
yield Service(item=ITEM_VERIFY + verify["id"])
|
||||
|
||||
|
||||
def discover_pbs_special_agent_storage(section):
|
||||
if "ds_usage" in section:
|
||||
for ds in section["ds_usage"]:
|
||||
yield Service(item=ITEM_DS_USAGE + ds["store"])
|
||||
if "status" in section:
|
||||
yield Service(item=ITEM_FS_ROOT)
|
||||
# yield Service(item=ITEM_FS_SWAP)
|
||||
|
||||
|
||||
|
||||
|
||||
def check_pbs_special_agent_storage(item: str, params: list, section: dict):
|
||||
if "error" in section:
|
||||
return
|
||||
if item.startswith(ITEM_DS_USAGE):
|
||||
for ds in section["ds_usage"]:
|
||||
if item.replace(ITEM_DS_USAGE, '') == ds["store"]:
|
||||
try:
|
||||
size_mb = float(ds['total']) / (1024 * 1024)
|
||||
avail_mb = float(ds['avail']) / (1024 * 1024)
|
||||
value_store = get_value_store()
|
||||
|
||||
yield from df_check_filesystem_single(
|
||||
value_store=value_store,
|
||||
mountpoint=ds["store"],
|
||||
filesystem_size=size_mb,
|
||||
free_space=avail_mb,
|
||||
reserved_space=0,
|
||||
inodes_total=None,
|
||||
inodes_avail=None,
|
||||
params=params,
|
||||
this_time=None,
|
||||
)
|
||||
except Exception:
|
||||
yield Result(
|
||||
state=State.UNKNOWN,
|
||||
summary="error checking datastore status"
|
||||
)
|
||||
if item == ITEM_FS_ROOT:
|
||||
try:
|
||||
fs_root = section["status"]["root"]
|
||||
size_mb = float(fs_root['total'])/1024/1024 #ds['total'] returning bytes instead of mb
|
||||
avail_mb = float(fs_root['avail'])/1024/1024 #ds['avail'] returning bytes instead of mb
|
||||
print(size_mb)
|
||||
value_store = get_value_store()
|
||||
|
||||
yield from df_check_filesystem_single(
|
||||
value_store=value_store,
|
||||
mountpoint="/root",
|
||||
filesystem_size=size_mb,
|
||||
free_space=avail_mb,
|
||||
reserved_space=0, # See df.py: ... if (filesystem_size is None) or (free_space is None) or (reserved_space is None): yield Result(state=State.OK, summary="no filesystem size information")
|
||||
inodes_total=None,
|
||||
inodes_avail=None,
|
||||
params=params,
|
||||
this_time=None,
|
||||
)
|
||||
except Exception as e:
|
||||
yield Result(
|
||||
state=State.UNKNOWN,
|
||||
summary=f"error checking root fs status"
|
||||
)
|
||||
|
||||
|
||||
agent_section_pbs_special_agent = AgentSection(
|
||||
name = "pbs_special_agent",
|
||||
parse_function = parse_pbs_special_agent,
|
||||
)
|
||||
|
||||
check_plugin_pbs_special_agent_status = CheckPlugin(
|
||||
name = "pbs_special_agent_status",
|
||||
sections = [ "pbs_special_agent" ],
|
||||
service_name = "PBS %s",
|
||||
discovery_function = discover_pbs_special_agent,
|
||||
check_function = check_pbs_special_agent,
|
||||
)
|
||||
check_plugin_pbs_special_agent_datastore = CheckPlugin(
|
||||
name = "pbs_special_agent_ds",
|
||||
sections = [ "pbs_special_agent" ],
|
||||
service_name = "PBS %s",
|
||||
discovery_function = discover_pbs_special_agent_storage,
|
||||
check_function = check_pbs_special_agent_storage,
|
||||
check_default_parameters=FILESYSTEM_DEFAULT_LEVELS,
|
||||
check_ruleset_name="filesystem",
|
||||
)
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import json
|
||||
import ssl
|
||||
import hashlib
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from urllib3.poolmanager import PoolManager
|
||||
|
||||
def fingerprint_checking_SSLSocket(_fingerprint:str):
|
||||
class SSLSocket(ssl.SSLSocket):
|
||||
fingerprint = _fingerprint.replace(":", "").lower()
|
||||
|
||||
def do_handshake(self, *args, **kw):
|
||||
res = super().do_handshake(*args, **kw)
|
||||
|
||||
# Get full certificate in DER format
|
||||
der_bytes = self.getpeercert(binary_form=True)
|
||||
|
||||
crt_sha256 = hashlib.sha256(der_bytes).hexdigest()
|
||||
|
||||
if crt_sha256.lower() != self.fingerprint.lower():
|
||||
raise ssl.SSLError(
|
||||
"Server %r certificate fingerprint (sha1) %s does not match %r"
|
||||
% (
|
||||
self.server_hostname,
|
||||
crt_sha256,
|
||||
self.fingerprint,
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
return SSLSocket
|
||||
|
||||
def api_get(session, url):
|
||||
try:
|
||||
r = session.get(url, timeout=50)
|
||||
|
||||
if r.status_code == 404:
|
||||
raise RuntimeError(f"API endpoint not found (404): {url}")
|
||||
|
||||
if not r.ok:
|
||||
raise RuntimeError(f"API error {r.status_code}: {url} - {r.text}")
|
||||
|
||||
return r.json().get("data", {})
|
||||
|
||||
except ConnectionError as e:
|
||||
raise RuntimeError(
|
||||
f"Connection refused to {url} (host or port unreachable)"
|
||||
) from e
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
raise RuntimeError(
|
||||
f"Timeout while connecting to {url}"
|
||||
) from e
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise RuntimeError(
|
||||
f"HTTP error from {url}: {e}"
|
||||
) from e
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise RuntimeError(
|
||||
f"Request failed for {url}: {e}"
|
||||
) from e
|
||||
|
||||
def create_session():
|
||||
session = requests.Session()
|
||||
|
||||
retry = Retry(
|
||||
total=3, # total retries
|
||||
connect=3, # connection retries
|
||||
read=3, # read retries
|
||||
backoff_factor=0.5, # 0.5s, 1s, 2s
|
||||
status_forcelist=[500, 502, 503, 504],
|
||||
allowed_methods=["GET"],
|
||||
raise_on_status=False,
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(
|
||||
max_retries=retry,
|
||||
pool_connections=5,
|
||||
pool_maxsize=5,
|
||||
)
|
||||
|
||||
session.mount("https://", adapter)
|
||||
session.mount("http://", adapter)
|
||||
|
||||
return session
|
||||
|
||||
class FingerprintAdapter(HTTPAdapter):
|
||||
def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
ctx.sslsocket_class = fingerprint_checking_SSLSocket(PBS_FINGERPRINT)
|
||||
|
||||
self.poolmanager = PoolManager(
|
||||
num_pools=connections,
|
||||
maxsize=maxsize,
|
||||
block=block,
|
||||
ssl_context=ctx,
|
||||
assert_hostname=False,
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser("agent_pbs_special_agent")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
help="PBS Host address or fqdn without https:// and port.",
|
||||
type=str,
|
||||
default="pbs")
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
help="PBS https port.",
|
||||
type=int,
|
||||
default="8007")
|
||||
parser.add_argument(
|
||||
"--fingerprint",
|
||||
help="Fingerprint of the PBS if not using valid cert.",
|
||||
type=str,
|
||||
default="")
|
||||
parser.add_argument(
|
||||
"--tokenid",
|
||||
help="API Token with audit permissions.",
|
||||
type=str,
|
||||
default="admin@pbs!checkmk")
|
||||
parser.add_argument(
|
||||
"--secret",
|
||||
help="Secret for API Token.",
|
||||
type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
PBS_HOST = args.host
|
||||
API_TOKEN_ID = args.tokenid
|
||||
PBS_PORT = args.port
|
||||
PBS_FINGERPRINT = args.fingerprint
|
||||
API_TOKEN_SECRET = args.secret
|
||||
|
||||
headers = {
|
||||
"Authorization": f"PBSAPIToken={API_TOKEN_ID}:{API_TOKEN_SECRET}"
|
||||
}
|
||||
# Disable SSL verification only if you use self-signed certs
|
||||
session = create_session()
|
||||
if PBS_FINGERPRINT != "":
|
||||
session.mount("https://", FingerprintAdapter())
|
||||
# session.verify = False
|
||||
session.headers.update(headers)
|
||||
|
||||
API_URL = f"https://{PBS_HOST}:{PBS_PORT}"
|
||||
try:
|
||||
return_json = {}
|
||||
return_json["version"] = api_get(session, f"{API_URL}/api2/json/version")
|
||||
return_json["status"] = api_get(session, f"{API_URL}/api2/json/nodes/localhost/status")
|
||||
return_json["tasks"] = api_get(session, f"{API_URL}/api2/json/nodes/localhost/tasks")
|
||||
return_json["ds_usage"] = api_get(session, f"{API_URL}/api2/json/status/datastore-usage")
|
||||
return_json["sync"] = api_get(session, f"{API_URL}/api2/json/admin/sync")
|
||||
return_json["gc"] = api_get(session, f"{API_URL}/api2/json/admin/gc")
|
||||
return_json["prune"] = api_get(session, f"{API_URL}/api2/json/admin/prune")
|
||||
return_json["verify"] = api_get(session, f"{API_URL}/api2/json/admin/verify")
|
||||
for d in return_json["ds_usage"]:
|
||||
# we do not need history data
|
||||
d.pop("history", None)
|
||||
print('<<<pbs_special_agent:sep(0)>>>')
|
||||
print(json.dumps(return_json))
|
||||
exit(0)
|
||||
except Exception as e:
|
||||
print('<<<pbs_special_agent:sep(0)>>>')
|
||||
print(json.dumps({"error": str(e)}))
|
||||
exit(0)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shebang needed only for editors
|
||||
|
||||
from cmk.rulesets.v1.form_specs import Dictionary, DictElement, String, Password, migrate_to_password, DefaultValue
|
||||
from cmk.rulesets.v1.rule_specs import SpecialAgent, Topic, Help, Title
|
||||
|
||||
def _formspec():
|
||||
return Dictionary(
|
||||
title=Title("PBS API-Token Login"),
|
||||
help_text=Help("This rule is used to showcase a special agent with configuration."),
|
||||
elements={
|
||||
"port": DictElement(
|
||||
required=True,
|
||||
parameter_form=String(
|
||||
title=Title("Port of the PBS API."),
|
||||
prefill=DefaultValue("8007"),
|
||||
),
|
||||
),
|
||||
"fingerprint": DictElement(
|
||||
required=True,
|
||||
parameter_form=String(
|
||||
title=Title("Fingerprint of the PBS"),
|
||||
prefill=DefaultValue(""),
|
||||
),
|
||||
),
|
||||
"tokenid": DictElement(
|
||||
required=True,
|
||||
parameter_form=String(
|
||||
title=Title("API Token with audit permissions."),
|
||||
),
|
||||
),
|
||||
"secret": DictElement(
|
||||
required=True,
|
||||
parameter_form=Password(
|
||||
title=Title("Secret for API Token."),
|
||||
migrate=migrate_to_password,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
rule_spec_pbs_special_agent = SpecialAgent(
|
||||
topic=Topic.CLOUD,
|
||||
name="pbs_special_agent",
|
||||
title=Title("Proxmox PBS"),
|
||||
parameter_form=_formspec
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shebang needed only for editors
|
||||
|
||||
from cmk.server_side_calls.v1 import noop_parser, SpecialAgentConfig, SpecialAgentCommand, HostConfig
|
||||
|
||||
def _agent_arguments(params, host_config: HostConfig):
|
||||
args = [
|
||||
"--host", host_config.name,
|
||||
"--port", params['port'],
|
||||
"--fingerprint", str(params['fingerprint']),
|
||||
"--tokenid", str(params['tokenid']),
|
||||
"--secret", params['secret'].unsafe()
|
||||
]
|
||||
yield SpecialAgentCommand(command_arguments=args)
|
||||
|
||||
special_agent_pbs_special_agent = SpecialAgentConfig(
|
||||
name="pbs_special_agent",
|
||||
parameter_parser=noop_parser,
|
||||
commands_function=_agent_arguments
|
||||
)
|
||||
Reference in New Issue
Block a user