Compare commits

...

17 Commits
v0.0.1 ... main

@ -13,16 +13,18 @@ sudo apt install python3-stdeb
git clone https://code.smolnet.org/micke/knotctl git clone https://code.smolnet.org/micke/knotctl
cd knotctl cd knotctl
python3 setup.py --command-packages=stdeb.command bdist_deb python3 setup.py --command-packages=stdeb.command bdist_deb
sudo dpkg -i deb_dist/knotctl_0.0.1-1_all.deb sudo dpkg -i deb_dist/knotctl_*_all.deb
``` ```
A prebuilt deb-package is also available from the release page: https://code.smolnet.org/micke/knotctl/releases/
## Shell completion ## Shell completion
For bash: add this to .bashrc For bash: add this to .bashrc
``` ```
source <(knotctl complete) source <(knotctl completion)
``` ```
For fish, run: For fish, run:
``` ```
knotctl complete --shell fish > ~/.config/fish/completions/knotctl.fish knotctl completion --shell fish > ~/.config/fish/completions/knotctl.fish
``` ```
For tcsh: add this to .cshrc For tcsh: add this to .cshrc
``` ```
@ -32,15 +34,15 @@ For zsh: add this to .zshrc
``` ```
autoload -U bashcompinit autoload -U bashcompinit
bashcompinit bashcompinit
source <(knotctl complete) source <(knotctl completion)
``` ```
## Usage ## Usage
``` ```
usage: knotctl [-h] [--json | --no-json] usage: knotctl [-h] [--json | --no-json]
{add,complete,config,delete,list,update} ... {add,completion,config,delete,list,update} ...
positional arguments: positional arguments:
{add,complete,config,delete,list,update} {add,completion,config,delete,list,update}
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -48,7 +50,7 @@ options:
``` ```
### ADD ### ADD
``` ```
usage: knotctl add [-h] -d DATA -n NAME -r RTYPE -t TTL -z ZONE usage: knotctl add [-h] -d DATA -n NAME -r RTYPE [-t TTL] -z ZONE
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -58,9 +60,9 @@ options:
-t TTL, --ttl TTL -t TTL, --ttl TTL
-z ZONE, --zone ZONE -z ZONE, --zone ZONE
``` ```
### COMPLETE ### COMPLETION
``` ```
usage: knotctl complete [-h] [-s SHELL] usage: knotctl completion [-h] [-s SHELL]
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -100,10 +102,14 @@ options:
``` ```
### UPDATE ### UPDATE
``` ```
usage: knotctl update [-h] -d DATA -n NAME -r RTYPE -t TTL -z ZONE usage: knotctl update [-h] -a [ARGUMENT ...] -d DATA -n NAME -r RTYPE [-t TTL]
-z ZONE
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-a [ARGUMENT ...], --argument [ARGUMENT ...]
Specify key - value pairs to be updated:
name=dns1.example.com.
-d DATA, --data DATA -d DATA, --data DATA
-n NAME, --name NAME -n NAME, --name NAME
-r RTYPE, --rtype RTYPE -r RTYPE, --rtype RTYPE

@ -5,10 +5,11 @@ import getpass
import json import json
import os import os
import sys import sys
from collections.abc import Sequence 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
from typing import Union from typing import Union
from urllib.parse import urlparse
import argcomplete import argcomplete
import requests import requests
@ -19,7 +20,7 @@ from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError
# Helper functions # Helper functions
def error(description: str, error: str) -> Sequence[dict]: def error(description: str, error: str) -> list[dict]:
response = [] response = []
reply = {} reply = {}
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406
@ -49,7 +50,7 @@ def nested_out(input, tabs="") -> str:
return string return string
def output(response: Sequence[dict], jsonout: bool = False): def output(response: list[dict], jsonout: bool = False):
try: try:
if jsonout: if jsonout:
print(json.dumps(response)) print(json.dumps(response))
@ -61,9 +62,18 @@ def output(response: Sequence[dict], jsonout: bool = False):
# Define the runner for each command # Define the runner for each command
def run_add(url: str, jsonout: bool, headers: dict): def run_add(url: str, jsonout: bool, headers: dict):
print(url) parsed = split_url(url)
response = requests.put(url, headers=headers) response = requests.put(url, headers=headers)
output(response.json(), jsonout) out = response.json()
if isinstance(out, list):
for record in out:
if (record["data"] == parsed["data"]
and record["name"] == parsed["name"]
and record["rtype"] == parsed["rtype"]):
output(record, jsonout)
break
else:
output(out, jsonout)
def run_complete(shell: Union[None, str]): def run_complete(shell: Union[None, str]):
@ -90,10 +100,12 @@ def run_config(
for need in needed: for need in needed:
if need == "": if need == "":
output( output(
error("Can not configure without {}".format(need), error(
"No {}".format(need))) "Can not configure without {}".format(need),
"No {}".format(need),
))
sys.exit(1) sys.exit(1)
config[need] = input("Enter {}:".format(need)) config[need] = input("Enter {}: ".format(need))
if not password: if not password:
try: try:
@ -115,9 +127,16 @@ def run_delete(url: str, jsonout: bool, headers: dict):
output(reply, jsonout) output(reply, jsonout)
def run_list(url: str, jsonout: bool, headers: dict): def run_list(url: str,
jsonout: bool,
headers: dict,
ret=False) -> Union[None, str]:
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
output(response.json(), jsonout) string = response.json()
if ret:
return string
else:
output(string, jsonout)
def run_update(url: str, jsonout: bool, headers: dict): def run_update(url: str, jsonout: bool, headers: dict):
@ -128,6 +147,7 @@ def run_update(url: str, jsonout: bool, headers: dict):
# Set up the url # Set up the url
def setup_url( def setup_url(
baseurl: str, baseurl: str,
arguments: Union[None, list[str]],
data: Union[None, str], data: Union[None, str],
name: Union[None, str], name: Union[None, str],
rtype: Union[None, str], rtype: Union[None, str],
@ -147,16 +167,27 @@ def setup_url(
url += "/{}".format(data) url += "/{}".format(data)
if ttl and data and zone and name and rtype: if ttl and data and zone and name and rtype:
url += "/{}".format(ttl) url += "/{}".format(ttl)
if data and zone and name and rtype and arguments:
url += "?"
for arg in arguments:
if not url.endswith("?"):
url += "&"
key, value = arg.split("=")
url += key + "=" + urllib.parse.quote_plus(value)
if ttl and (not rtype or not name or not zone): if ttl and (not rtype or not name or not zone):
output( output(
error("ttl only makes sense with rtype, name and zone", error(
"Missing parameter")) "ttl only makes sense with rtype, name and zone",
"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("rtype only makes sense with name and zone", error(
"Missing parameter")) "rtype only makes sense with name and zone",
"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"))
@ -164,6 +195,38 @@ def setup_url(
return url return url
def split_url(url: str) -> dict:
parsed = urlparse(url, allow_fragments=False)
path = parsed.path
query = parsed.query
arguments: Union[None, list[str]] = query.split("&")
path_arr = path.split("/")
data: Union[None, str] = None
name: Union[None, str] = None
rtype: Union[None, str] = None
ttl: Union[None, str] = None
zone: Union[None, str] = None
if len(path_arr) > 2:
zone = path_arr[2]
if len(path_arr) > 4:
name = path_arr[4]
if len(path_arr) > 5:
rtype = path_arr[5]
if len(path_arr) > 6:
data = path_arr[6]
if len(path_arr) > 7:
ttl = path_arr[7]
return {
"arguments": arguments,
"data": data,
"name": name,
"rtype": rtype,
"ttl": ttl,
"zone": zone,
}
# Entry point to program # Entry point to program
def main() -> int: def main() -> int:
# Grab user input # Grab user input
@ -174,10 +237,10 @@ def main() -> int:
addcmd.add_argument("-d", "--data", required=True) addcmd.add_argument("-d", "--data", required=True)
addcmd.add_argument("-n", "--name", required=True) addcmd.add_argument("-n", "--name", required=True)
addcmd.add_argument("-r", "--rtype", required=True) addcmd.add_argument("-r", "--rtype", required=True)
addcmd.add_argument("-t", "--ttl", required=True) addcmd.add_argument("-t", "--ttl")
addcmd.add_argument("-z", "--zone", required=True) addcmd.add_argument("-z", "--zone", required=True)
completecmd = subparsers.add_parser("complete") completecmd = subparsers.add_parser("completion")
completecmd.add_argument("-s", "--shell") completecmd.add_argument("-s", "--shell")
configcmd = subparsers.add_parser("config") configcmd = subparsers.add_parser("config")
@ -198,15 +261,22 @@ def main() -> int:
listcmd.add_argument("-z", "--zone") listcmd.add_argument("-z", "--zone")
updatecmd = subparsers.add_parser("update") updatecmd = subparsers.add_parser("update")
updatecmd.add_argument(
"-a",
"--argument",
nargs="*",
help="Specify key - value pairs to be updated: name=dns1.example.com.",
required=True,
)
updatecmd.add_argument("-d", "--data", required=True) updatecmd.add_argument("-d", "--data", required=True)
updatecmd.add_argument("-n", "--name", required=True) updatecmd.add_argument("-n", "--name", required=True)
updatecmd.add_argument("-r", "--rtype", required=True) updatecmd.add_argument("-r", "--rtype", required=True)
updatecmd.add_argument("-t", "--ttl", required=True) updatecmd.add_argument("-t", "--ttl")
updatecmd.add_argument("-z", "--zone", required=True) updatecmd.add_argument("-z", "--zone", required=True)
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
args = parser.parse_args() args = parser.parse_args()
if args.command == "complete": if args.command == "completion":
run_complete(args.shell) run_complete(args.shell)
return 0 return 0
@ -218,7 +288,7 @@ def main() -> int:
mkdir(config_basepath) mkdir(config_basepath)
if args.command == "config": if args.command == "config":
run_config(args.baseurl, args.username, args.password) run_config(config_filename, args.baseurl, args.username, args.password)
return 0 return 0
if not isfile(config_filename): if not isfile(config_filename):
@ -238,13 +308,41 @@ def main() -> int:
except KeyError: except KeyError:
output(response.json()) output(response.json())
return 1 return 1
except requests.exceptions.JSONDecodeError:
output(
error("Could not decode api response as JSON", "Could not decode"))
return 1
headers = {"Authorization": "Bearer {}".format(token)} headers = {"Authorization": "Bearer {}".format(token)}
# Route based on command # Route based on command
ttl = None ttl = None
if 'ttl' in args: if "ttl" in args:
ttl = args.ttl ttl = args.ttl
url = setup_url(baseurl, args.data, args.name, args.rtype, ttl, args.zone) if args.command != "update":
args.argument = None
if args.command == "add" and not ttl:
if args.zone.endswith("."):
zname = args.zone
else:
zname = args.zone + "."
soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
soa_json = run_list(soa_url, True, headers, ret=True)
ttl = soa_json[0]["ttl"]
try:
url = setup_url(
baseurl,
args.argument,
args.data,
args.name,
args.rtype,
ttl,
args.zone,
)
except AttributeError:
parser.print_help(sys.stderr)
return 1
try: try:
if args.command == "add": if args.command == "add":
run_add(url, args.json, headers) run_add(url, args.json, headers)

@ -5,8 +5,8 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup( setuptools.setup(
name="knotctl", name="knotctl",
version="0.0.1", version="0.0.6",
packages = setuptools.find_packages(), packages=setuptools.find_packages(),
author="Micke Nordin", author="Micke Nordin",
author_email="hej@mic.ke", author_email="hej@mic.ke",
description="A cli for knotapi.", description="A cli for knotapi.",

Loading…
Cancel
Save