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
cd knotctl
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
For bash: add this to .bashrc
```
source <(knotctl complete)
source <(knotctl completion)
```
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
```
@ -32,15 +34,15 @@ For zsh: add this to .zshrc
```
autoload -U bashcompinit
bashcompinit
source <(knotctl complete)
source <(knotctl completion)
```
## Usage
```
usage: knotctl [-h] [--json | --no-json]
{add,complete,config,delete,list,update} ...
{add,completion,config,delete,list,update} ...
positional arguments:
{add,complete,config,delete,list,update}
{add,completion,config,delete,list,update}
options:
-h, --help show this help message and exit
@ -48,7 +50,7 @@ options:
```
### 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:
-h, --help show this help message and exit
@ -58,9 +60,9 @@ options:
-t TTL, --ttl TTL
-z ZONE, --zone ZONE
```
### COMPLETE
### COMPLETION
```
usage: knotctl complete [-h] [-s SHELL]
usage: knotctl completion [-h] [-s SHELL]
options:
-h, --help show this help message and exit
@ -100,10 +102,14 @@ options:
```
### 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:
-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
-n NAME, --name NAME
-r RTYPE, --rtype RTYPE

@ -5,10 +5,11 @@ import getpass
import json
import os
import sys
from collections.abc import Sequence
import urllib.parse
from os import environ, mkdir
from os.path import isdir, isfile, join
from typing import Union
from urllib.parse import urlparse
import argcomplete
import requests
@ -19,7 +20,7 @@ from simplejson.errors import JSONDecodeError as SimplejsonJSONDecodeError
# Helper functions
def error(description: str, error: str) -> Sequence[dict]:
def error(description: str, error: str) -> list[dict]:
response = []
reply = {}
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406
@ -49,7 +50,7 @@ def nested_out(input, tabs="") -> str:
return string
def output(response: Sequence[dict], jsonout: bool = False):
def output(response: list[dict], jsonout: bool = False):
try:
if jsonout:
print(json.dumps(response))
@ -61,9 +62,18 @@ def output(response: Sequence[dict], jsonout: bool = False):
# Define the runner for each command
def run_add(url: str, jsonout: bool, headers: dict):
print(url)
parsed = split_url(url)
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]):
@ -90,10 +100,12 @@ def run_config(
for need in needed:
if need == "":
output(
error("Can not configure without {}".format(need),
"No {}".format(need)))
error(
"Can not configure without {}".format(need),
"No {}".format(need),
))
sys.exit(1)
config[need] = input("Enter {}:".format(need))
config[need] = input("Enter {}: ".format(need))
if not password:
try:
@ -115,9 +127,16 @@ def run_delete(url: str, jsonout: bool, headers: dict):
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)
output(response.json(), jsonout)
string = response.json()
if ret:
return string
else:
output(string, jsonout)
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
def setup_url(
baseurl: str,
arguments: Union[None, list[str]],
data: Union[None, str],
name: Union[None, str],
rtype: Union[None, str],
@ -147,16 +167,27 @@ def setup_url(
url += "/{}".format(data)
if ttl and data and zone and name and rtype:
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):
output(
error("ttl only makes sense with rtype, name and zone",
"Missing parameter"))
error(
"ttl only makes sense with rtype, name and zone",
"Missing parameter",
))
sys.exit(1)
if rtype and (not name or not zone):
output(
error("rtype only makes sense with name and zone",
"Missing parameter"))
error(
"rtype only makes sense with name and zone",
"Missing parameter",
))
sys.exit(1)
if name and not zone:
output(error("name only makes sense with a zone", "Missing parameter"))
@ -164,6 +195,38 @@ def setup_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
def main() -> int:
# Grab user input
@ -174,10 +237,10 @@ def main() -> int:
addcmd.add_argument("-d", "--data", required=True)
addcmd.add_argument("-n", "--name", 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)
completecmd = subparsers.add_parser("complete")
completecmd = subparsers.add_parser("completion")
completecmd.add_argument("-s", "--shell")
configcmd = subparsers.add_parser("config")
@ -198,15 +261,22 @@ def main() -> int:
listcmd.add_argument("-z", "--zone")
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("-n", "--name", 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)
argcomplete.autocomplete(parser)
args = parser.parse_args()
if args.command == "complete":
if args.command == "completion":
run_complete(args.shell)
return 0
@ -218,7 +288,7 @@ def main() -> int:
mkdir(config_basepath)
if args.command == "config":
run_config(args.baseurl, args.username, args.password)
run_config(config_filename, args.baseurl, args.username, args.password)
return 0
if not isfile(config_filename):
@ -238,13 +308,41 @@ def main() -> int:
except KeyError:
output(response.json())
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)}
# Route based on command
ttl = None
if 'ttl' in args:
if "ttl" in args:
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:
if args.command == "add":
run_add(url, args.json, headers)

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

Loading…
Cancel
Save