From 6150895b8691d4eb10314d31eae3f60503758e51 Mon Sep 17 00:00:00 2001 From: Dhya Date: Thu, 3 Oct 2024 14:35:12 -0700 Subject: [PATCH] initial commit --- .gitignore | 1 + CertInfo.py | 49 ++ README.md | 93 ++++ __pycache__/config.cpython-311.pyc | Bin 0 -> 966 bytes certmanager.py | 742 +++++++++++++++++++++++++++++ config.py | 21 + database.ini.EXAMPLE | 6 + requirements.txt | 6 + testconn.py | 27 ++ 9 files changed, 945 insertions(+) create mode 100644 .gitignore create mode 100644 CertInfo.py create mode 100644 README.md create mode 100644 __pycache__/config.cpython-311.pyc create mode 100644 certmanager.py create mode 100644 config.py create mode 100644 database.ini.EXAMPLE create mode 100644 requirements.txt create mode 100644 testconn.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03f1c90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +database.ini diff --git a/CertInfo.py b/CertInfo.py new file mode 100644 index 0000000..3e5e594 --- /dev/null +++ b/CertInfo.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +""" +see: https://www.postgresqltutorial.com/postgresql-python/connect/ + +Dhya Sat, 05 Mar 2022 01:00:10 -0700 +""" + +class CertInfo: + """Class to contain certificate info""" + def __init__(self, name): + self.name = name + + def set_x509_cert(self, x509_cert): + self.x509_cert = x509_cert + + def get_x509_cert(self): + return self.x509_cert + + def set_cn(self, common_name): + self.common_name = common_name + + def get_cn(self): + return self.common_name + + def set_issuer(self, issuer): + self.issuer = issuer + + def get_issuer(self): + return self.issuer + + def set_not_valid_before(self, not_valid_before): + self.not_valid_before = not_valid_before + + def get_not_valid_before(self): + return self.not_valid_before + + def set_not_valid_after(self, not_valid_after): + self.not_valid_after = not_valid_after + + def get_not_valid_after(self): + return self.not_valid_after + + def set_last_checked(self, last_checked): + self.last_checked = last_checked + + def get_last_checked(self): + return self.last_checked + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f069551 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Cert Manager + +Cert Manager is a program to manage SSL certificates for multiple domains. It stores information in a PostgreSQL database. It retreives live certificates from the web and updates the database records accordingly. It reports on expiring or expired certificates, and it renews certifcates. + + +## Requirements + +cryptography +psycopg v.3 +```bash +# Install with the virtualenv +# remove existing venv: +rm -rf venv +# setup venv +virtualenv venv && source ./venv/bin/activate && pip install -r requirements.txt + + +# Install without virtualenv (not recommended): +pip install cryptography psycopg[binary] +``` + +## Configuration + +- Settings stored in `database.ini` + + +## cert_manager database + +- Contains master list of all domains +- "isActive" to indicate whether a domain is currently active + + +## Usage +```bash +--help # show help +--check-live # check info for live certficate +--checkall # check SSL certficates for all managed domains +--list-active # List all active domains +--critical # List all expired or soon-to-expire domains +--info # Show info about a domain +--refresh # Refresh cert info in database for domain +--refresh-all # Refresh cert info in database for all domains +--renew # Renew certificate for domain +``` + +## Create database and user + +Add this line to the bottom of /etc/postgresql/15/main/pg_hba.conf: +```conf +local cert_manager cert_manager peer +``` +Make sure port in /etc/postgresql/17/main/postgresql.conf agrees with port in cert-manager-db-setup + +sudo grep port /etc/postgresql/17/main/postgresql.conf +```bash +# reload postgresql: +sudo systemctl reload postgresql +# setup db: +/usr/bin/cp -f cert-manager.sql cert-manager-db-setup /tmp/ +cd /tmp +./cert-manager-db-setup +# test db access +psql --username=cert_manager --host=127.0.0.1 --port=5433 --dbname=cert_manager --command="SELECT * FROM main LIMIT 3" +``` +## Create database (Windows) + +- assumes Windows version of PostgreSQL installed +```ps1 +dropdb -U postgres --echo --if-exists cert_manager +dropuser -U postgres --echo --if-exists cert_manager +createuser -U postgres --echo --pwprompt cert_manager +createdb -U postgres --echo --owner=cert_manager cert_manager +psql -U postgres --username=cert_manager --dbname=cert_manager --echo-all --file=cert-manager.sql +``` + +## certbot output + +- output from subprocess.run() needs to be captured and processed +```bash +certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136.ini -d 'example.org' +``` +```text +Saving debug log to /var/log/letsencrypt/letsencrypt.log +Requesting a certificate for example.org +Waiting 60 seconds for DNS changes to propagate + +Successfully received certificate. +Certificate is saved at: /etc/letsencrypt/live/example.org/fullchain.pem +Key is saved at: /etc/letsencrypt/live/example.org/privkey.pem +This certificate expires on 2023-01-29. +These files will be updated when the certificate renews. +Certbot has set up a scheduled task to automatically renew this certificate in the background. +``` diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc6ddb6a7c43b62298d8e9d196703c80e5b8a80b GIT binary patch literal 966 zcmZ`%&rcIU6rS0iP+CkQXe%5xMA0V7YCPzHXo5mwAb=qzQX^*BnYNpBckAq;Alsx8 z5=+8?c!7flPN?CZ@K2zmhGr5I6HnYWAznB+vrSvj__Finefz%m=Dm6IIgy9~CgPX4 zIVb?|i-VRh9LdQQwrv9h5G(>4W+4PfLDD+R3P_#<8TFXcGLXg}xlYc?&l<$T9 z-&iy}JYqQ(jg?%_&lBvuE}E^xJWOFrWb0%I@s*6@$Szx&KIV-Q&U$xk4x7H^I@;>(vgWwHmUEXJ zq*;#U7qGT^r>y0yBF?CU_eUjy4MckihWE;A#!=bwvF*|B@nsX2cqXdkTw)tO?c$M+ zVdIQK1v5w0R)|uH#!CgXNV#pWc)7XrS!$7--qmf#*G>HydkLP73M+<=@EhI4#J^=5 zj*-ViFRl0m*U`=B(TtQtTZs45s1Wg!e^TiX}sA$^M;~Jk9zqE$R*C|W9qbP)OOKQ5kDaO#qe2r c_n2a5Lm0*Y96kWo&VHekfW0C3kBSoi0eueJwg3PC literal 0 HcmV?d00001 diff --git a/certmanager.py b/certmanager.py new file mode 100644 index 0000000..8ff3527 --- /dev/null +++ b/certmanager.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python3 +""" +see: https://www.postgresqltutorial.com/postgresql-python/connect/ + +Options: + add # add to database + remove # remove from database + active # set status to active + inactive # set status to inactive + info # get host info from database + check # check live cert for and update db entry if in db + checkall # check live cert for all active hosts + critical # list all certs with status 'critical' + renew # renew a host + +Dhya Thu, 03 Oct 2024 14:32:32 -0700 +""" + +import argparse +import concurrent.futures +import re +import shutil +import ssl +import sys +from dataclasses import dataclass +from datetime import datetime +from datetime import timezone +from socket import socket, AF_INET, SOCK_STREAM +from typing import Final + +import psycopg +from cryptography import x509 +from cryptography.x509.oid import ExtensionOID +from cryptography.x509.oid import NameOID +from psycopg import sql + +# import os +# os.chdir("/usr/local/lib/certmanager") +from config import config + +DT_FMT: Final[str] = '%F %T %Z' + + +def fqdn_type(host_name): + """ + Used by argparse to validate fqdn input + Makes sure supplied host name is a valid fqdn + """ + if not 1 < len(host_name) < 253: + return False + parts = host_name.split('.') + fqdn = re. \ + compile(r'^[a-z0-9]([a-z-0-9-]{0,61}[a-z0-9])?$', + re.IGNORECASE) + if all(fqdn.match(part) for part in parts): + return host_name + print("Error: \"", host_name, "\" is not a valid hostname.", sep='') + sys.exit(1) + + +@dataclass +class Host: + """ + Class to encapsulate a host + All fields are optional except {host_name}, e.g.: + h1 = Host("example.com") + """ + host_name: str # non-default argument must come first + host_id: int = None # primary key, autoincremented + common_name: str = None + issuer: str = None + not_valid_before: datetime = None + not_valid_after: datetime = None + active: bool = False # a host can have active or inactive status + checked: datetime = None # date of most recent cert check + check_status: str = None # status of most recent cert check + check_err: str = None # error (if any) of most recent cert check + renewed: datetime = None # datetime of most recent cert renewal + renew_status: str = None # status of most recent cert renewal + renew_out: str = None # output of most recent cert renewal + renew_err: str = None # error output of most recent cert renewal + + @classmethod + def from_row( + cls, *, host_id, host_name, common_name, issuer, + not_valid_before, not_valid_after, active, + checked, check_status, check_err, renewed, renew_status, + renew_out, renew_err + ): + """ + Row factory to process psycopg SQL values into Host class + """ + return cls( + host_id=host_id, + host_name=host_name, + common_name=common_name, + issuer=issuer, + not_valid_before=not_valid_before, + not_valid_after=not_valid_after, + active=active, + checked=checked, + check_status=check_status, + check_err=check_err, + renewed=renewed, + renew_status=renew_status, + renew_out=renew_out, + renew_err=renew_err + ) + + @classmethod + def row_factory(cls, cursor): + """use cursor""" + columns = [column.name for column in cursor.description] + + def make_row(values): + row = dict(zip(columns, values)) + return cls.from_row(**row) + + return make_row + + +@dataclass +class San: + """ + Class to encapsulate Subject Alternative Names data + """ + name: str # non-default argument must come first + host_id: int = None # primary key, autoincremented + status: str = None # status of most recent cert check + + @classmethod + def from_row( + cls, *, host_id, name, status + ): + """ + Row factory to process psycopg SQL values into SAN class + """ + return cls( + host_id=host_id, + name=name, + status=status + ) + + @classmethod + def row_factory(cls, cursor): + """use cursor""" + columns = [column.name for column in cursor.description] + + def make_row(values): + row = dict(zip(columns, values)) + return cls.from_row(**row) + + return make_row + + +def db_retrieve(query): + """ + Retrieves SSL certificate data + """ + params = config() + with psycopg.connect(**params) as conn: + with conn.cursor() as cur: + # next two lines are for debuging db queries + # print(query.as_string(conn)) + # return + cur.execute(query) + return cur.fetchall() + + +def db_execute(query): + """ + Execute SQL statement + """ + params = config() + with psycopg.connect(**params) as conn: + with conn.cursor() as cur: + # next lines are for debuging db queries + # print(query.as_string(conn)) + # sys.exit(0) + # return + cur.execute(query) + conn.commit() + + +def is_managed(host_name): + """ + Validate that a host is managed by this application, returns boolean + """ + # TODO: why is this called multiple times? + params = config() + with psycopg.connect(**params) as conn: + with conn.cursor() as cur: + query = ("SELECT EXISTS(SELECT 1 FROM main " + f"WHERE host_name='{host_name}')") + cur.execute(query) + if result := cur.fetchone()[0]: + return result + return False + + +def cert_error_db_update(host, err): + """ + Update database with cert check error data + """ + query = sql.SQL( + """ + UPDATE {table} SET + checked = {f1}, + check_status = {f2}, + check_err = {f3} + WHERE host_name = {f4} + """ + ).format( + # .Identifier = PGSQL identifier, e.g. table & column names, uses + # double quotes + # .Literal = PGSQL literal, e.g. field values, uses single quotes + table=sql.Identifier('main'), + f1=sql.Literal(datetime.now(timezone.utc)), + f2=sql.Literal('failure'), + f3=sql.Literal(str(err)), + f4=sql.Literal(host) + ) + db_execute(query) + + +def get_sans(host): + """ + Retreive host's SANs from database + """ + # print(f"get sans: {host}") + # SELECT 'name' FROM 'san' WHERE 'host_id' = (select host_id from main where host_name = {host}) + + params = config() + with psycopg.connect(**params) as conn: + with conn.cursor() as cur: + query = ("SELECT * FROM san WHERE host_id = " + f"(SELECT host_id FROM main WHERE host_name = '{host}')" + ) + # next two lines are for debuging db queries + # print(f"get_sans query: {query}") + # return + cur.execute(query) + return cur.fetchone() + + +def cert_db_update(host, cert): + """ + Update database host record with cert info + """ + + # TODO: process SANs from cert + + query = sql.SQL( + """ + UPDATE {table} SET + not_valid_before = {f1}, + not_valid_after = {f2}, + common_name = {f3}, + issuer = {f4}, + checked = {f5}, + check_status = {f6} + WHERE host_name = {f7} + """ + ).format( + # .Identifier = PGSQL identifier, e.g. table & column names, uses + # double quotes + # .Literal = PGSQL literal, e.g. field values, uses single quotes + table=sql.Identifier('main'), + f1=sql.Literal( + cert.not_valid_before_utc.replace(tzinfo=timezone.utc)), + f2=sql.Literal( + cert.not_valid_after_utc.replace(tzinfo=timezone.utc)), + f3=sql.Literal( + cert.subject.get_attributes_for_oid( + NameOID.COMMON_NAME)[0].value), + f4=sql.Literal( + cert.issuer.get_attributes_for_oid( + NameOID.ORGANIZATION_NAME)[0].value), + f5=sql.Literal(datetime.now(timezone.utc)), + f6=sql.Literal('success'), + f7=sql.Literal(host) + ) + + db_execute(query) + + managed_sans = get_sans(host) + + san = cert. \ + extensions. \ + get_extension_for_oid(ExtensionOID. + SUBJECT_ALTERNATIVE_NAME) + + cert_sans = san.value.get_values_for_type(x509.DNSName) + + if managed_sans is not None: + for _ in managed_sans: + pass + + # This will require a separete UPDATE/INSERT for dns_names table + + # print("DNS Names:", dnames) + # INSERT INTO san (host_id, name) + # VALUES ((select host_id from main where host_name = 'workin.com'), '*.workin.com'); + + +class GetCertException(Exception): + """ + Custom exception thrown by get_cert() + Raise with: raise get_cert_exception("Exception text") + """ + + +def get_cert(host_to_check): + """ + Check live certificate for host + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + try: + sock = socket(AF_INET, SOCK_STREAM, 0) + sock.settimeout(5.0) # 5 second timeout + ssock = context.wrap_socket(sock, server_hostname=host_to_check) + ssock.connect((host_to_check, 443)) + pem_data = ssl.DER_cert_to_PEM_cert(ssock.getpeercert(True)) + cert = x509.load_pem_x509_certificate(str.encode(pem_data)) + + # EOF occurred in violation of protocol (_ssl.c:1123) + # [Errno -2] Name or service not known + # [Errno -3] Temporary failure in name resolution + # [Errno -5] No address associated with hostname + # [Errno 110] Connection timed out + # [Errno 111] Connection refused + # [Errno 113] No route to host + except Exception as err: + if is_managed(host_to_check): + cert_error_db_update(host_to_check, err) + raise GetCertException(err) + + # Handle self-signed certificates which lack common name/org. name + if not cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME): + err_string = "Certificate has no Common Name" + if not cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME): + err_string = "Certificate has no Common Name and no Organization Name" + if is_managed(host_to_check): + cert_error_db_update(host_to_check, err_string) + raise GetCertException(err_string) + + return cert, ssock + + +def handle_add(args): + """ + Add a host + """ + if is_managed(args.host): + print("Host \'", args.host, "\' is already managed by this application.") + return + + query = sql.SQL( + "INSERT INTO {table} (host_name) VALUES ({f1})" + ).format( + table=sql.Identifier('main'), + f1=sql.Literal(args.host) + ) + db_execute(query) + + +def handle_remove(args): + """ + Remove a host + """ + if not is_managed(args.host): + print("Host \'", args.host, "\' is not managed by this application.") + return + + query = sql.SQL( + "DELETE FROM {table} WHERE {host_name} = {f1}" + ).format( + table=sql.Identifier('main'), + host_name=sql.Identifier('host_name'), + f1=sql.Literal(args.host) + ) + db_execute(query) + + +def handle_list_all(args): + """ + Handle list-all + + List all hosts and their check status + """ + query = sql.SQL( + "SELECT {host_name}, {check_status} FROM {table}" + ).format( + host_name=sql.Identifier('host_name'), + check_status=sql.Identifier('check_status'), + table=sql.Identifier('main') + ) + + if len(db_retrieve(query)) == 0: + print("No hosts found in the database") + else: + result = db_retrieve(query) + rlist = list(result) + rlist.sort() + for i in rlist: + print("{:<36} Check status:".format(i[0].decode("utf-8")), i[1].decode("utf-8")) + + +def handle_list_active(args): + """ + Handle list-active + + List hosts marked as active + """ + query = sql.SQL( + """ + SELECT {host_name} FROM {table} + WHERE {active} IS TRUE + """ + ).format( + host_name=sql.Identifier('host_name'), + table=sql.Identifier('main'), + active=sql.Identifier('active') + ) + actives = list(zip(*db_retrieve(query)))[0] + alist = list(actives) + alist.sort() + for a in alist: + print(a.decode()) + + +def handle_active(args): + """ + Set host status to active + """ + if not is_managed(args.host): + print(f"Host \'{args.host}\' is not managed by this application.", + "Exiting.") + return + + query = sql.SQL( + """ + UPDATE {table} SET + active = {f1} + WHERE host_name = {f2} + """ + ).format( + table=sql.Identifier('main'), + f1=sql.Literal('true'), + f2=sql.Literal(args.host) + ) + db_execute(query) + + +def handle_inactive(args): + """ + Set host status to inactive + """ + if not is_managed(args.host): + print(f"Host \'{args.host}\' is not managed by this application.", + "Exiting.") + return + + query = sql.SQL( + """ + UPDATE {table} SET + active = {f1} + WHERE host_name = {f2} + """ + ).format( + table=sql.Identifier('main'), + f1=sql.Literal('false'), + f2=sql.Literal(args.host) + ) + db_execute(query) + + +def handle_info(args): + """ + Return database info on host + """ + # print("info", args.host) + if not is_managed(args.host): + print("Host \'", args.host, "\' is not managed by this application.") + sys.exit(1) + + params = config() + with psycopg.connect(**params) as conn: + with conn.cursor(row_factory=Host.row_factory) as cur: + query = sql.SQL( + """ + SELECT * FROM main WHERE host_name = %s + """ + ) + cur.execute(query, (args.host,)) + row = cur.fetchone() + #sans = get_sans(args.host) + + # stringer = dt.strftime(DT_FMT) if dt else lambda _: None; + print( + f"Host name: {row.host_name} \n", + f"Cert Common Name: {row.common_name} \n", + f"Cert Issuer: {row.issuer} \n", + "Cert Not Valid Before: ", + row.not_valid_before.astimezone(timezone.utc).strftime(DT_FMT) + if row.not_valid_before is not None else '', + " (", + row.not_valid_before.strftime(DT_FMT) + if row.not_valid_before is not None else '', ")\n", + "Cert Not Valid After: ", + row.not_valid_after.astimezone(timezone.utc).strftime(DT_FMT) + if row.not_valid_after is not None else '', + " (", + row.not_valid_after.strftime(DT_FMT) + if row.not_valid_after is not None else '', + ")\n", + "Active: ", row.active, "\n", + "Checked date: ", + row.checked.strftime( + DT_FMT) if row.checked is not None else '', + "\n", + "Check status: ", row.check_status, "\n", + "Check error msg: ", row.check_err, "\n", + "Renewed date: ", + row.renewed.strftime( + DT_FMT) if row.renewed is not None else '', + "\n", + sep='' + ) + # if sans is not None: + # print("SANs: ", end="") + # for n in sans: + # print(f"{n} ", end="") + # return + # print(f"No SANs information for {args.host}") + + +def handle_check(args): + """ + Check live certificate for host and refresh database entry + """ + # TODO: don't call is_managed() repeatedly here + + print("Getting live cert info for:", args.host) + + print(f"in handle_check(), is_managed={is_managed(args.host)}") + + if not is_managed(args.host): + print("NOTICE: Host \'", args.host, + "\' is not managed by this application.") + + try: + cert, ssock = get_cert(args.host) + except GetCertException as err: + print("Error getting certificate:", err) + sys.exit(1) + + # if not is_managed(args.host): + print("SSL protocol version:", ssock.version()) + print("Issuer:", + cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value) + print("Subject CN:", + cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value) + print("Not valid before:", cert.not_valid_before_utc) + print("Not valid after:", cert.not_valid_after_utc) + san = cert.extensions.get_extension_for_oid( + ExtensionOID.SUBJECT_ALTERNATIVE_NAME) + dnames = san.value.get_values_for_type(x509.DNSName) + print("DNS Names:") + for name in dnames: + print(" ", name) + # return + + if is_managed(args.host): + cert_db_update(args.host, cert) + + +def handle_checkall(args): + """ + Handle --check-all + + Refreshes info for all certificates + """ + active_hosts_query = sql.SQL( + """ + SELECT {host_name} FROM {table} + WHERE {active} IS TRUE + """ + ).format( + host_name=sql.Identifier('host_name'), + table=sql.Identifier('main'), + active=sql.Identifier('active') + ) + + hosts = [] + for host in db_retrieve(active_hosts_query): + hosts.append(host[0]) + with concurrent.futures.ThreadPoolExecutor(max_workers=12) as executor: + future_to_host = {executor.submit(get_cert, h): h for h in hosts} + for future in concurrent.futures.as_completed(future_to_host): + h = future_to_host[future] + try: + print("Getting live cert info for:", h) + # get_cert() returns tuple[Certificate, SSLSocket] + cert = future.result()[0] + cert_db_update(h, cert) + except GetCertException as err: + print("Error getting certificate for: ", h, ": ", err, sep="") + except Exception as exc: + print('%r generated an exception: %s' % (h, exc)) + # else: + # print() + + +def handle_critical(args): + """ + Handle --critical + + Return list of expiring and recently expired active hosts + """ + # print("critical", args) + # return + CRITICAL_INTERVAL: Final[str] = '14 DAY' + query = sql.SQL( + """ + SELECT {host_name}, {nva} FROM {table} + WHERE {nva} < now() + interval {interval} + """ + ).format( + host_name=sql.Identifier('host_name'), + table=sql.Identifier('main'), + nva=sql.Identifier('not_valid_after'), + interval=sql.Literal(CRITICAL_INTERVAL) + ) + if len(db_retrieve(query)) == 0: + print("No hosts are listed as critical at this time") + else: + for i in db_retrieve(query): + print("{:<30} Expires:".format(i[0].decode("utf-8"), i[1])) + + +def handle_renew(args): + """ + Handle --renew + + Renews a certificate + """ + if certbot_cmd := shutil.which('certbot'): + renew_cmd = [ + f'{certbot_cmd}', + '--cert-name', f'{args.host}', + '--dns-rfc2136', + '--dns-rfc2136-credentials', + '/etc/letsencrypt/rfc2136.ini', + '-d', f'{args.host}' + ] + # subprocess.run(certbotcmd, check=False) + print("Not implemented yet") + print("Renew command:", *renew_cmd) + return True + + sys.stderr.write("\nThe certbot binary was not found in $PATH\n" + "Please make sure certbot is installed.\n" + "This program will now exit.\n") + sys.exit(1) + + +args = "" +parser = argparse.ArgumentParser() + +# this is to deal with "AttributeError: 'Namespace' object has no attribute 'func'" issue +parser.set_defaults(func=lambda _: parser.print_help()) + +subparsers = parser.add_subparsers(help='Functions') + +# "add" parser +add_parser = subparsers.add_parser('add', help='add host') +add_parser.add_argument('-active', required=False, + action="store_true", help='set host status to active') +add_parser.add_argument('-inactive', required=False, + action="store_true", help='set host status to inactive') +add_parser.add_argument('host', type=str, help='host name') +add_parser.set_defaults(func=handle_add) + +# "remove" parser +remove_parser = subparsers.add_parser('remove', help='remove host') +remove_parser.add_argument('host', type=str, help='host name') +remove_parser.set_defaults(func=handle_remove) + +# "list-all" parser +listall_parser = subparsers.add_parser( + 'list-all', help='list all hosts and their check status') +listall_parser.set_defaults(func=handle_list_all) + +# "list-active" parser +listactive_parser = subparsers.add_parser( + 'list-active', help='list hosts with active status') +listactive_parser.set_defaults(func=handle_list_active) + +# "active" parser +active_parser = subparsers.add_parser('active', help='set host to active') +active_parser.add_argument('host', type=str, help='host name') +active_parser.set_defaults(func=handle_active) + +# "inactive" parser +inactive_parser = subparsers.add_parser( + 'inactive', help='set host to inactive') +inactive_parser.add_argument('host', type=str, help='host name') +inactive_parser.set_defaults(func=handle_inactive) + +# "info" parser +info_parser = subparsers.add_parser('info', help='display info about host') +info_parser.add_argument('host', type=str, help='host name') +info_parser.set_defaults(func=handle_info) + +# "check" parser +check_parser = subparsers.add_parser( + 'check', help='check live SSL cert for host') +check_parser.add_argument('host', type=str, help='host name') +check_parser.set_defaults(func=handle_check) + +# "checkall" parser +checkall_parser = subparsers.add_parser( + 'check-all', help='check live SSL certs for all active hosts') +checkall_parser.set_defaults(func=handle_checkall) + +# "critical" parser +critical_parser = subparsers.add_parser( + 'critical', help='list active hosts that are critical') +critical_parser.set_defaults(func=handle_critical) + +# "renew" parser +renew_parser = subparsers.add_parser('renew', help='renew SSL cert for host') +renew_parser.add_argument('host', type=str, help='host name') +renew_parser.set_defaults(func=handle_renew) + +args = parser.parse_args() + +if args.func: + args.func(args) diff --git a/config.py b/config.py new file mode 100644 index 0000000..98f003b --- /dev/null +++ b/config.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +# https://www.postgresqltutorial.com/postgresql-python/connect/ + +from configparser import ConfigParser + +def config(cf='database.ini', section='postgresql'): + parser = ConfigParser() + parser.read(cf) + + # get section, default to postgresql + db = {} + if parser.has_section(section): + params = parser.items(section) + for param in params: + db[param[0]] = param[1] + else: + raise Exception('Section {0} not found in the {1} file'.format(section, filename)) + + return db + diff --git a/database.ini.EXAMPLE b/database.ini.EXAMPLE new file mode 100644 index 0000000..492d90d --- /dev/null +++ b/database.ini.EXAMPLE @@ -0,0 +1,6 @@ +[postgresql] +host=localhost +port=5433 +dbname=cert_manager +user=cert_manager +password=1234567890abcde diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..aa279a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +cffi==1.17.1 +cryptography==43.0.1 +psycopg==3.2.3 +psycopg-binary==3.2.3 +pycparser==2.22 +typing_extensions==4.12.2 diff --git a/testconn.py b/testconn.py new file mode 100644 index 0000000..02db7e9 --- /dev/null +++ b/testconn.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +# https://www.postgresqltutorial.com/postgresql-python/connect/ + +import psycopg +from config import config + +conn = None + +try: + params = config() + conn = psycopg.connect(**params) + cur = conn.cursor() + cur.execute( + "SELECT * from main LIMIT 10" + ) + # cur.fetchone() + for record in cur: + print(record) + conn.commit() + cur.close() +except (Exception, psycopg.DatabaseError) as error: + print(error) +finally: + if conn is not None: + conn.close() +