Early support for auditlog
This commit is contained in:
parent
3787ad12df
commit
c26a46e9a4
2 changed files with 110 additions and 43 deletions
|
@ -7,7 +7,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from os import environ, mkdir
|
from os import environ, mkdir
|
||||||
from os.path import isdir, isfile, join
|
from os.path import isdir, isfile, join, split
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import requests
|
||||||
import yaml
|
import yaml
|
||||||
from requests.models import HTTPBasicAuth
|
from requests.models import HTTPBasicAuth
|
||||||
from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError
|
from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -45,8 +46,7 @@ def nested_out(input, tabs="") -> str:
|
||||||
string += "{}\n".format(input)
|
string += "{}\n".format(input)
|
||||||
elif isinstance(input, dict):
|
elif isinstance(input, dict):
|
||||||
for key, value in input.items():
|
for key, value in input.items():
|
||||||
string += "{}{}: {}".format(tabs, key,
|
string += "{}{}: {}".format(tabs, key, nested_out(value, tabs + " "))
|
||||||
nested_out(value, tabs + " "))
|
|
||||||
elif isinstance(input, list):
|
elif isinstance(input, list):
|
||||||
for entry in input:
|
for entry in input:
|
||||||
string += "{}\n{}".format(tabs, nested_out(entry, tabs + " "))
|
string += "{}\n{}".format(tabs, nested_out(entry, tabs + " "))
|
||||||
|
@ -70,15 +70,57 @@ def run_add(url: str, jsonout: bool, headers: dict):
|
||||||
out = response.json()
|
out = response.json()
|
||||||
if isinstance(out, list):
|
if isinstance(out, list):
|
||||||
for record in out:
|
for record in out:
|
||||||
if (record["data"] == parsed["data"]
|
if (
|
||||||
and record["name"] == parsed["name"]
|
record["data"] == parsed["data"]
|
||||||
and record["rtype"] == parsed["rtype"]):
|
and record["name"] == parsed["name"]
|
||||||
|
and record["rtype"] == parsed["rtype"]
|
||||||
|
):
|
||||||
output(record, jsonout)
|
output(record, jsonout)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
output(out, jsonout)
|
output(out, jsonout)
|
||||||
|
|
||||||
|
|
||||||
|
def run_audit(url: str, jsonout: bool, headers: dict):
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
string = response.content.decode("utf-8")
|
||||||
|
if jsonout:
|
||||||
|
out = []
|
||||||
|
lines = string.splitlines()
|
||||||
|
index = 0
|
||||||
|
text = ""
|
||||||
|
timestamp = ""
|
||||||
|
while index < len(lines):
|
||||||
|
line = lines[index]
|
||||||
|
index += 1
|
||||||
|
cur_has_timestamp = line.startswith("[")
|
||||||
|
next_has_timestamp = index < len(lines) and lines[index].startswith(
|
||||||
|
"["
|
||||||
|
)
|
||||||
|
# Simple case, just one line with timestamp
|
||||||
|
if cur_has_timestamp and next_has_timestamp:
|
||||||
|
text = ":". join(line.split(':')[1:])
|
||||||
|
timestamp = line.split(']')[0].split('[')[1]
|
||||||
|
out.append({'timestamp': timestamp, 'text': text})
|
||||||
|
text = ""
|
||||||
|
timestamp = ""
|
||||||
|
elif cur_has_timestamp:
|
||||||
|
timestamp = line.split(']')[0].split('[')[1]
|
||||||
|
text = ":". join(line.split(':')[1:])
|
||||||
|
elif next_has_timestamp:
|
||||||
|
text += f'\n{line}'
|
||||||
|
out.append({'timestamp': timestamp, 'text': text})
|
||||||
|
text = ""
|
||||||
|
timestamp = ""
|
||||||
|
else:
|
||||||
|
text += f'\n{line}'
|
||||||
|
|
||||||
|
else:
|
||||||
|
out = string
|
||||||
|
|
||||||
|
output(out, jsonout)
|
||||||
|
|
||||||
|
|
||||||
def run_complete(shell: Union[None, str]):
|
def run_complete(shell: Union[None, str]):
|
||||||
if not shell or shell in ["bash", "zsh"]:
|
if not shell or shell in ["bash", "zsh"]:
|
||||||
os.system("register-python-argcomplete knotctl")
|
os.system("register-python-argcomplete knotctl")
|
||||||
|
@ -98,7 +140,7 @@ def run_config(
|
||||||
config = {"baseurl": baseurl, "username": username, "password": password}
|
config = {"baseurl": baseurl, "username": username, "password": password}
|
||||||
needed = []
|
needed = []
|
||||||
if context:
|
if context:
|
||||||
symlink = f'{config_filename}-{context}'
|
symlink = f"{config_filename}-{context}"
|
||||||
found = os.path.isfile(symlink)
|
found = os.path.isfile(symlink)
|
||||||
if os.path.islink(config_filename):
|
if os.path.islink(config_filename):
|
||||||
os.remove(config_filename)
|
os.remove(config_filename)
|
||||||
|
@ -118,7 +160,8 @@ def run_config(
|
||||||
error(
|
error(
|
||||||
"Can not configure without {}".format(need),
|
"Can not configure without {}".format(need),
|
||||||
"No {}".format(need),
|
"No {}".format(need),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
config[need] = input("Enter {}: ".format(need))
|
config[need] = input("Enter {}: ".format(need))
|
||||||
|
|
||||||
|
@ -142,10 +185,7 @@ def run_delete(url: str, jsonout: bool, headers: dict):
|
||||||
output(reply, jsonout)
|
output(reply, jsonout)
|
||||||
|
|
||||||
|
|
||||||
def run_list(url: str,
|
def run_list(url: str, jsonout: bool, headers: dict, ret=False) -> Union[None, str]:
|
||||||
jsonout: bool,
|
|
||||||
headers: dict,
|
|
||||||
ret=False) -> Union[None, str]:
|
|
||||||
response = requests.get(url, headers=headers)
|
response = requests.get(url, headers=headers)
|
||||||
string = response.json()
|
string = response.json()
|
||||||
if ret:
|
if ret:
|
||||||
|
@ -176,7 +216,7 @@ def setup_url(
|
||||||
url += "/{}".format(zone)
|
url += "/{}".format(zone)
|
||||||
if name and zone:
|
if name and zone:
|
||||||
if name.endswith(zone.rstrip(".")):
|
if name.endswith(zone.rstrip(".")):
|
||||||
name += '.'
|
name += "."
|
||||||
url += "/records/{}".format(name)
|
url += "/records/{}".format(name)
|
||||||
if zone and name and rtype:
|
if zone and name and rtype:
|
||||||
url += "/{}".format(rtype)
|
url += "/{}".format(rtype)
|
||||||
|
@ -197,14 +237,16 @@ def setup_url(
|
||||||
error(
|
error(
|
||||||
"ttl only makes sense with rtype, name and zone",
|
"ttl only makes sense with rtype, name and zone",
|
||||||
"Missing parameter",
|
"Missing parameter",
|
||||||
))
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if rtype and (not name or not zone):
|
if rtype and (not name or not zone):
|
||||||
output(
|
output(
|
||||||
error(
|
error(
|
||||||
"rtype only makes sense with name and zone",
|
"rtype only makes sense with name and zone",
|
||||||
"Missing parameter",
|
"Missing parameter",
|
||||||
))
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if name and not zone:
|
if name and not zone:
|
||||||
output(error("name only makes sense with a zone", "Missing parameter"))
|
output(error("name only makes sense with a zone", "Missing parameter"))
|
||||||
|
@ -293,7 +335,11 @@ def main() -> int:
|
||||||
* https://en.wikipedia.org/wiki/Zone_file
|
* https://en.wikipedia.org/wiki/Zone_file
|
||||||
"""
|
"""
|
||||||
# Grab user input
|
# Grab user input
|
||||||
parser = argparse.ArgumentParser(description=description, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter)
|
parser = argparse.ArgumentParser(
|
||||||
|
description=description,
|
||||||
|
epilog=epilog,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
parser.add_argument("--json", action=argparse.BooleanOptionalAction)
|
parser.add_argument("--json", action=argparse.BooleanOptionalAction)
|
||||||
subparsers = parser.add_subparsers(dest="command")
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
|
@ -305,6 +351,9 @@ def main() -> int:
|
||||||
addcmd.add_argument("-t", "--ttl")
|
addcmd.add_argument("-t", "--ttl")
|
||||||
addcmd.add_argument("-z", "--zone", required=True)
|
addcmd.add_argument("-z", "--zone", required=True)
|
||||||
|
|
||||||
|
audit_description = "Audit the log file for errors."
|
||||||
|
subparsers.add_parser("audit", description=audit_description)
|
||||||
|
|
||||||
complete_description = "Generate shell completion script."
|
complete_description = "Generate shell completion script."
|
||||||
completecmd = subparsers.add_parser("completion", description=complete_description)
|
completecmd = subparsers.add_parser("completion", description=complete_description)
|
||||||
completecmd.add_argument("-s", "--shell")
|
completecmd.add_argument("-s", "--shell")
|
||||||
|
@ -330,24 +379,34 @@ def main() -> int:
|
||||||
listcmd.add_argument("-r", "--rtype")
|
listcmd.add_argument("-r", "--rtype")
|
||||||
listcmd.add_argument("-z", "--zone", required=True)
|
listcmd.add_argument("-z", "--zone", required=True)
|
||||||
|
|
||||||
update_description = "Update a record in the zone. The record must exist in the zone.\n"
|
update_description = (
|
||||||
update_description += "In this case --data, --name, --rtype and --ttl switches are used\n"
|
"Update a record in the zone. The record must exist in the zone.\n"
|
||||||
update_description += "for searching for the appropriate record, while the --argument\n"
|
)
|
||||||
|
update_description += (
|
||||||
|
"In this case --data, --name, --rtype and --ttl switches are used\n"
|
||||||
|
)
|
||||||
|
update_description += (
|
||||||
|
"for searching for the appropriate record, while the --argument\n"
|
||||||
|
)
|
||||||
update_description += "switches are used for updating the record."
|
update_description += "switches are used for updating the record."
|
||||||
update_epilog = """Available arguments are:
|
update_epilog = """Available arguments are:
|
||||||
data: New record data.
|
data: New record data.
|
||||||
name: New record domain name.
|
name: New record domain name.
|
||||||
rtype: New record type.
|
rtype: New record type.
|
||||||
ttl: New record time to live (TTL)."""
|
ttl: New record time to live (TTL)."""
|
||||||
updatecmd = subparsers.add_parser("update", description=update_description, epilog=update_epilog, formatter_class=argparse.RawDescriptionHelpFormatter )
|
updatecmd = subparsers.add_parser(
|
||||||
|
"update",
|
||||||
|
description=update_description,
|
||||||
|
epilog=update_epilog,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
updatecmd.add_argument(
|
updatecmd.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
"--argument",
|
"--argument",
|
||||||
nargs="*",
|
nargs="*",
|
||||||
action="append",
|
action="append",
|
||||||
metavar="KEY=VALUE",
|
metavar="KEY=VALUE",
|
||||||
help=
|
help="Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated",
|
||||||
"Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated",
|
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
updatecmd.add_argument("-d", "--data", required=True)
|
updatecmd.add_argument("-d", "--data", required=True)
|
||||||
|
@ -370,8 +429,9 @@ def main() -> int:
|
||||||
mkdir(config_basepath)
|
mkdir(config_basepath)
|
||||||
|
|
||||||
if args.command == "config":
|
if args.command == "config":
|
||||||
run_config(config_filename, args.context, args.baseurl, args.username,
|
run_config(
|
||||||
args.password)
|
config_filename, args.context, args.baseurl, args.username, args.password
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if not isfile(config_filename):
|
if not isfile(config_filename):
|
||||||
|
@ -392,8 +452,7 @@ def main() -> int:
|
||||||
output(response.json())
|
output(response.json())
|
||||||
return 1
|
return 1
|
||||||
except requests.exceptions.JSONDecodeError:
|
except requests.exceptions.JSONDecodeError:
|
||||||
output(
|
output(error("Could not decode api response as JSON", "Could not decode"))
|
||||||
error("Could not decode api response as JSON", "Could not decode"))
|
|
||||||
return 1
|
return 1
|
||||||
headers = {"Authorization": "Bearer {}".format(token)}
|
headers = {"Authorization": "Bearer {}".format(token)}
|
||||||
|
|
||||||
|
@ -411,20 +470,22 @@ def main() -> int:
|
||||||
soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
|
soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
|
||||||
soa_json = run_list(soa_url, True, headers, ret=True)
|
soa_json = run_list(soa_url, True, headers, ret=True)
|
||||||
ttl = soa_json[0]["ttl"]
|
ttl = soa_json[0]["ttl"]
|
||||||
|
if args.command == "audit":
|
||||||
try:
|
url = baseurl + "/user/auditlog"
|
||||||
url = setup_url(
|
else:
|
||||||
baseurl,
|
try:
|
||||||
args.argument,
|
url = setup_url(
|
||||||
args.data,
|
baseurl,
|
||||||
args.name,
|
args.argument,
|
||||||
args.rtype,
|
args.data,
|
||||||
ttl,
|
args.name,
|
||||||
args.zone,
|
args.rtype,
|
||||||
)
|
ttl,
|
||||||
except AttributeError:
|
args.zone,
|
||||||
parser.print_help(sys.stderr)
|
)
|
||||||
return 1
|
except AttributeError:
|
||||||
|
parser.print_help(sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.command == "add":
|
if args.command == "add":
|
||||||
|
@ -435,9 +496,15 @@ def main() -> int:
|
||||||
run_list(url, args.json, headers)
|
run_list(url, args.json, headers)
|
||||||
elif args.command == "update":
|
elif args.command == "update":
|
||||||
run_update(url, args.json, headers)
|
run_update(url, args.json, headers)
|
||||||
|
elif args.command == "audit":
|
||||||
|
run_audit(url, args.json, headers)
|
||||||
|
else:
|
||||||
|
parser.print_help(sys.stderr)
|
||||||
|
return 1
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
output(error(e, "Could not connect to server"))
|
||||||
except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
|
except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
|
||||||
output(
|
output(error("Could not decode api response as JSON", "Could not decode"))
|
||||||
error("Could not decode api response as JSON", "Could not decode"))
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ classifiers=[
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
requires-python= ">=3.9"
|
requires-python= ">=3.9"
|
||||||
version = "0.0.7"
|
version = "0.0.8"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argcomplete==2.0.0",
|
"argcomplete==2.0.0",
|
||||||
|
|
Loading…
Add table
Reference in a new issue