|
|
@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|