ansible-base/roles/tlscert_acme_inwx/files/inwx

741 lines
15 KiB
Text
Raw Normal View History

2023-11-20 16:01:49 +01:00
#!/usr/bin/env python3
from typing import List
import os as _os
import sys as _sys
import json as _json
import http.client as _http_client
import argparse as _argparse
import pathlib as _pathlib
import time as _time
2024-06-01 18:41:52 +02:00
def convey(x, fs):
y = x
for f in fs:
y = f(y)
return y
def string_coin(
template : str,
arguments : dict
):
result = template
for (key, value, ) in arguments.items():
result = result.replace("{{%s}}" % key, value)
return result
def file_read(
path : str
):
handle = open(path, "r")
content = handle.read()
handle.close()
return content
2023-11-20 16:01:49 +01:00
def log(
messsage : str
):
_sys.stderr.write("-- %s\n" % messsage)
def path_read(
thing,
steps : List[str]
):
position = thing
for step in steps:
if (not (step in position)):
raise ValueError("missing key '%s'" % ".".join(steps))
position = position[step]
return position
def http_call(
request : dict,
) -> dict:
connection = (
{
"http": (lambda: _http_client.HTTPConnection(request["url"]["host"], request["url"]["port"])),
"https": (lambda: _http_client.HTTPSConnection(request["url"]["host"], request["url"]["port"])),
}[request["url"]["scheme"]]
)()
connection.request(
request["method"],
("/" + request["url"]["path"]),
request["data"],
request["headers"]
)
response_ = connection.getresponse()
response = {
"status": response_.status,
"headers": dict(response_.getheaders()),
"data": response_.read(),
}
return response
2024-06-01 18:41:52 +02:00
_conf_data = None
def conf_schema(
):
return {
"type": "object",
"properties": {
"url": {
"type": "object",
"properties": {
},
"additionalProperties": {
"type": "object",
"properties": {
"scheme": {
"type": "string",
},
"host": {
"type": "string",
},
"port": {
"type": "number",
},
"path": {
"type": "string",
},
},
"additionalProperties": False,
"required": [
"host",
]
},
"required": [
]
},
"environment": {
"type": "string",
},
"account": {
"type": "object",
"properties": {
"username": {
"type": "string",
},
"password": {
"type": "string",
},
},
"additionalProperties": False,
"required": [
]
}
2023-11-20 16:01:49 +01:00
},
2024-06-01 18:41:52 +02:00
"additionalProperties": False,
"required": [
],
2023-11-20 16:01:49 +01:00
}
2024-06-01 18:41:52 +02:00
2023-11-20 16:01:49 +01:00
def conf_load(
path : str
):
global _conf_data
2024-06-01 18:41:52 +02:00
conf_data_raw = (
_json.loads(file_read(path))
if _os.path.exists(path) else
{}
)
for pair in conf_data_raw.get("url", {}).items():
if ("host" in pair[1]):
pass
else:
raise ValueError("flawed conf: missing mandatory value 'host' for url entry '%s'" % pair[0])
_conf_data = {
"url": convey(
(
{
"test": {
"scheme": "https",
"host": "api.ote.domrobot.com",
"port": 443,
"path": "jsonrpc/"
},
"production": {
"scheme": "https",
"host": "api.domrobot.com",
"port": 443,
"path": "jsonrpc/"
}
}
|
conf_data_raw.get("url", {})
),
[
lambda x: x.items(),
lambda pairs: map(
lambda pair: (
pair[0],
{
"scheme": pair[1].get("scheme", "https"),
"host": pair[1]["host"],
"port": pair[1].get("port", 443),
"path": pair[1].get("path", "jsonrpc/"),
}
),
pairs
),
dict,
]
),
"environment": conf_data_raw.get("environment", "production"),
"account": {
"username": conf_data_raw.get("account", {}).get("username", None),
"password": conf_data_raw.get("account", {}).get("password", None),
}
}
# print(_json.dumps(_conf_data, indent = "\t"))
2023-11-20 16:01:49 +01:00
def conf_get(
path : str
):
global _conf_data
return path_read(_conf_data, path.split("."))
def api_call(
environment : str,
accesstoken : str,
category : str,
action : str,
data,
):
url = conf_get("url." + environment)
# input_["lang"] = "de"
request_headers = {
"Content-Type": "application/json",
}
if (accesstoken is not None):
request_headers["Cookie"] = ("domrobot=%s" % (accesstoken, ))
else:
pass
request_data_decoded = {
"method": (category + "." + action),
"params": data,
}
request = {
"url": url,
"method": "POST",
"headers": request_headers,
"data": _json.dumps(request_data_decoded),
}
# log("[>>] %s" % _json.dumps(request, indent = "\t"))
response = http_call(request)
# log("[<<] %s" % _json.dumps(response, indent = "\t"))
if (not (response["status"] == 200)):
raise ValueError("API call failed with status %u: %s" % (response["status"], response["data"], ))
else:
output_data_decoded = _json.loads(response["data"])
result = (output_data_decoded["resData"] if ("resData" in output_data_decoded) else {})
if ("Set-Cookie" in response["headers"]):
result["_accesstoken"] = response["headers"]["Set-Cookie"].split("; ")[0].split("=")[1]
else:
pass
if (output_data_decoded["code"] == 2002):
raise ValueError("wrong use: %s" % str(output_data_decoded))
else:
return result
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02.html#account.login
'''
2023-11-20 16:01:49 +01:00
def api_macro_login(
environment : str,
username : str,
password : str
):
if ((username is None) or (password is None)):
raise ValueError("username or password not given")
else:
response = (
api_call(
environment,
None,
"account",
"login",
{
"user": username,
"pass": password,
}
)
)
return response["_accesstoken"]
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02.html#account.logout
'''
2023-11-20 16:01:49 +01:00
def api_macro_logout(
environment : str,
accesstoken : str
):
response = api_call(
environment,
accesstoken,
"account",
"logout",
{
}
)
return None
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02.html#account.info
'''
2023-11-20 16:01:49 +01:00
def api_macro_info(
environment : str,
username : str,
password : str
):
accesstoken = api_macro_login(environment, username, password)
info = api_call(
environment,
accesstoken,
"account",
"info",
{
}
)
api_macro_logout(environment, accesstoken)
return info
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.info
'''
2023-11-20 16:01:49 +01:00
def api_macro_list(
environment : str,
username : str,
password : str,
domain : str
):
accesstoken = api_macro_login(environment, username, password)
info = api_call(
environment,
accesstoken,
"nameserver",
"info",
{
"domain": domain,
}
)
api_macro_logout(environment, accesstoken)
return info
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.info
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.createRecord
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.updateRecord
'''
2023-11-20 16:01:49 +01:00
def api_macro_save(
environment : str,
username : str,
password : str,
2024-06-01 18:41:52 +02:00
domain_base : str,
domain_path,
2023-11-20 16:01:49 +01:00
type_ : str,
content : str
):
accesstoken = api_macro_login(environment, username, password)
info = api_call(
environment,
accesstoken,
"nameserver",
"info",
{
2024-06-01 18:41:52 +02:00
"domain": domain_base,
2023-11-20 16:01:49 +01:00
}
)
matching = list(
filter(
2024-06-01 18:41:52 +02:00
lambda record: (
(
(
(domain_path is None)
and
(record["name"] == domain_base)
)
or
(
(domain_path is not None)
and
(record["name"] == (domain_path + "." + domain_base))
)
)
and
(record["type"] == type_)
),
2023-11-20 16:01:49 +01:00
info["record"]
)
)
count = len(matching)
if (count == 0):
result = api_call(
environment,
accesstoken,
"nameserver",
"createRecord",
{
2024-06-01 18:41:52 +02:00
"domain": domain_base,
"name": domain_path,
2023-11-20 16:01:49 +01:00
"type": type_,
"content": content,
}
)
id_ = result["id"]
log("created record %u" % id_)
elif (count == 1):
id_ = matching[0]["id"]
result = api_call(
environment,
accesstoken,
"nameserver",
"updateRecord",
{
"id": id_,
"content": content,
}
)
log("updated record %u" % id_)
else:
log("found multiple records with this name and type")
api_macro_logout(environment, accesstoken)
2024-06-01 18:41:52 +02:00
'''
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.info
@see https://www.inwx.de/de/help/apidoc/f/ch02s15.html#nameserver.deleteRecord
'''
def api_macro_delete(
environment : str,
username : str,
password : str,
domain_base : str,
domain_path,
type_
2023-11-20 16:01:49 +01:00
):
2024-06-01 18:41:52 +02:00
accesstoken = api_macro_login(environment, username, password)
info = api_call(
environment,
accesstoken,
"nameserver",
"info",
{
"domain": domain_base,
}
)
matching = list(
filter(
lambda record: (
(
(
(domain_path is None)
and
(record["name"] == domain_base)
)
or
(
(domain_path is not None)
and
(record["name"] == (domain_path + "." + domain_base))
)
)
and
(
(type_ is None)
or
(record["type"] == type_)
)
),
info["record"]
)
)
for entry in matching:
id_ = entry["id"]
result = api_call(
environment,
accesstoken,
"nameserver",
"deleteRecord",
{
"id": id_,
}
)
api_macro_logout(environment, accesstoken)
def main(
):
## args
argument_parser = _argparse.ArgumentParser(
2023-11-20 16:01:49 +01:00
description = "INWX CLI Frontend"
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-c",
"--conf",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "conf",
default = _os.path.join(str(_pathlib.Path.home()), ".inwx-conf.json"),
metavar = "<conf>",
help = "path to configuration file",
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-e",
"--environment",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "environment",
metavar = "<environment>",
default = None,
2024-06-01 18:41:52 +02:00
help = "environment to use; one of the keys in the 'url' node of the configuration; overwrites the configuration value",
2023-11-20 16:01:49 +01:00
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-u",
"--username",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "username",
metavar = "<username>",
default = None,
help = "username; overwrites the configuration value",
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-p",
"--password",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "password",
metavar = "<password>",
default = None,
help = "password; overwrites the configuration value",
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-d",
"--domain",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "domain",
default = None,
metavar = "<domain>",
help = "the domain to work with"
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
"-t",
"--type",
type = str,
dest = "type",
default = None,
metavar = "<type>",
help = "the record type (A, AAAA, TXT, …)"
)
argument_parser.add_argument(
"-v",
"--value",
type = str,
dest = "value",
default = None,
metavar = "<value>",
help = "value for the record"
)
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-x",
"--challenge-prefix",
2024-06-01 18:41:52 +02:00
type = str,
2023-11-20 16:01:49 +01:00
dest = "challenge_prefix",
metavar = "<challenge-prefix>",
default = "_acme-challenge",
help = "which subdomain to use for ACME challanges",
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
"-w",
"--delay",
type = float,
2024-06-01 18:41:52 +02:00
dest = "delay",
2023-11-20 16:01:49 +01:00
default = 60.0,
metavar = "<delay>",
help = "seconds to wait at end of certbot auth hook",
)
2024-06-01 18:41:52 +02:00
argument_parser.add_argument(
2023-11-20 16:01:49 +01:00
type = str,
2024-06-01 18:41:52 +02:00
dest = "action",
choices = [
"conf-schema",
"info",
"list",
"save",
"delete",
"certbot-hook",
],
2023-11-20 16:01:49 +01:00
metavar = "<action>",
2024-06-01 18:41:52 +02:00
help = string_coin(
"action to execute; options:\n{{options}}",
{
"options": convey(
[
{"name": "conf-schema", "requirements": []},
{"name": "info", "requirements": []},
{"name": "list", "requirements": ["<domain>"]},
{"name": "save", "requirements": ["<domain>", "<type>", "<value>"]},
{"name": "delete", "requirements": ["<domain>"]},
{"name": "certbot-hook", "requirements": []},
],
[
lambda x: map(
lambda entry: string_coin(
"{{name}}{{macro_requirements}}",
{
"name": entry["name"],
"macro_requirements": (
""
if (len(entry["requirements"]) <= 0) else
string_coin(
" (requires: {{requirements}})",
{
"requirements": ",".join(entry["requirements"]),
}
)
),
}
),
x
),
" | ".join,
]
)
}
),
2023-11-20 16:01:49 +01:00
)
2024-06-01 18:41:52 +02:00
args = argument_parser.parse_args()
2023-11-20 16:01:49 +01:00
2024-06-01 18:41:52 +02:00
## conf
conf_load(args.conf)
2023-11-20 16:01:49 +01:00
2024-06-01 18:41:52 +02:00
## vars
environment = (args.environment or conf_get("environment"))
account_username = (args.username or conf_get("account.username"))
account_password = (args.password or conf_get("account.password"))
domain_parts = (None if (args.domain is None) else args.domain.split("."))
domain_base = (None if (domain_parts is None) else ".".join(domain_parts[-2:]))
domain_path = (None if ((domain_parts is None) or (len(domain_parts[:-2]) <= 0)) else ".".join(domain_parts[:-2]))
2023-11-20 16:01:49 +01:00
2024-06-01 18:41:52 +02:00
## exec
if (args.action == "conf-schema"):
print(_json.dumps(conf_schema(), indent = "\t"))
elif (args.action == "info"):
if (account_username is None):
raise ValueError("account username required")
else:
if (account_password is None):
raise ValueError("account password required")
else:
result = api_macro_info(
environment,
account_username,
account_password
)
print(_json.dumps(result, indent = "\t"))
elif (args.action == "list"):
if (account_username is None):
raise ValueError("account username required")
else:
if (account_password is None):
raise ValueError("account password required")
else:
if (args.domain is None):
raise ValueError("domain required")
else:
result = api_macro_list(
environment,
account_username,
account_password,
domain_base
)
print(_json.dumps(result, indent = "\t"))
elif (args.action == "save"):
if (account_username is None):
raise ValueError("account username required")
else:
if (account_password is None):
raise ValueError("account password required")
else:
if (args.domain is None):
raise ValueError("domain required")
else:
if (args.type is None):
raise ValueError("type required")
else:
if (args.value is None):
raise ValueError("value required")
else:
api_macro_save(
environment,
account_username,
account_password,
domain_base,
domain_path,
args.type,
args.value
)
elif (args.action == "delete"):
if (account_username is None):
raise ValueError("account username required")
else:
if (account_password is None):
raise ValueError("account password required")
else:
if (args.domain is None):
raise ValueError("domain required")
else:
api_macro_delete(
environment,
account_username,
account_password,
domain_base,
domain_path,
args.type
)
elif (args.action == "certbot-hook"):
if (account_username is None):
raise ValueError("account username required")
else:
if (account_password is None):
raise ValueError("account password required")
else:
domain_full_parts = _os.environ["CERTBOT_DOMAIN"].split(".")
domain_base = ".".join(domain_full_parts[-2:])
domain_path_stripped = ".".join(domain_full_parts[:-2])
domain_path = (args.challenge_prefix + "." + domain_path_stripped)
type_ = "TXT"
content = _os.environ["CERTBOT_VALIDATION"]
api_macro_save(
environment,
account_username,
account_password,
domain_base,
domain_path,
type_,
content
)
_time.sleep(args.delay)
# print(_json.dumps(result, indent = "\t"))
2023-11-20 16:01:49 +01:00
else:
2024-06-01 18:41:52 +02:00
log("unhandled action '%s'" % (args.action, ))
2023-11-20 16:01:49 +01:00
try:
main()
except ValueError as error:
2024-06-01 18:41:52 +02:00
_sys.stderr.write("-- %s\n" % str(error))
2023-11-20 16:01:49 +01:00