Last active
October 21, 2023 23:08
-
-
Save haxwithaxe/1aa59ed9049e878c1b59a671b56d4c5b to your computer and use it in GitHub Desktop.
A certdeploy compatible script to update certs in a proxmox cluster.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""A command line tool to update TLS certificates on Proxmox clusters. | |
Note: | |
When running for the first time or after the certificates have expired set | |
`verify_tls` in the config to `false`. | |
The default config locations are: | |
- ``/cerdeploy/scripts/proxmox_certupdater.toml`` | |
- ``/etc/proxmox_certupdater.toml`` | |
- ``/usr/local/etc/proxmox_certupdater.toml`` | |
A custom config file location can be given as the only argument on the command | |
line or as an environment variable (`PROXMOX_CERTUPDATER_CONF_FILE`). | |
Example Config File: | |
```toml | |
api_domain = "proxmox.example.com" | |
token = "<your API key>" | |
token_name = "certdeployer_token" | |
user = "certdeployer@pve" | |
key_filename = "/certdeploy/certs/example.com/privkey.pem" | |
cert_filename = "/certdeploy/certs/example.com/fullchain.pem" | |
nodes = [ | |
"pve0", | |
"pve1", | |
"pve2" | |
] | |
verify_tls = true | |
``` | |
""" | |
import logging | |
import os | |
import pathlib | |
import platform | |
import sys | |
import time | |
from dataclasses import dataclass | |
try: | |
import tomllib | |
except ImportError: | |
import tomli as tomllib | |
from proxmoxer import ProxmoxAPI | |
import requests | |
__license__ = 'GPLv3' | |
__authors__ = ['haxwithaxe'] | |
__copyright__ = 'haxwithaxe 2023' | |
DEFAULT_CONFIG_FILES = ( | |
pathlib.Path('/cerdeploy/scripts/proxmox_certupdater.toml'), | |
pathlib.Path('/etc/proxmox_certupdater.toml'), | |
pathlib.Path('/usr/local/etc/proxmox_certupdater.toml'), | |
) | |
logging.basicConfig() | |
log = logging.getLogger(name=__file__.split('.', 1)[0]) | |
@dataclass | |
class Config: | |
api_domain: str | |
"""The domain where the API is located.""" | |
token: str | |
"""The secret API token.""" | |
token_name: str | |
"""The token name. The part after the ``!`` in what Proxmox shows during | |
API token creation.""" | |
user: str | |
"""The username that corresponds to the token. The user needs | |
``Sys.Modify`` permissions on ``/nodes/<node name>`` for all nodes in `nodes`.""" | |
key_filename: pathlib.Path | |
"""The TLS key filename. The full or relative path to the file.""" | |
cert_filename: pathlib.Path | |
"""The TLS certificate filename. The full or relative path to the file.""" | |
nodes: list[str] | |
"""A list of nodes to update certificates for.""" | |
verify_tls: bool = True | |
"""Verify the certificate of the API if `True`. Defaults to `True`. Set to | |
`False` for the first run before valid certs are installed.""" | |
retries: int = 3 | |
retry_delay: int = 60 | |
@classmethod | |
def load(cls, config_filename: pathlib.Path) -> 'Config': | |
with pathlib.Path(config_filename).open('rb') as config_file: | |
config_dict = tomllib.load(config_file) | |
cert = pathlib.Path(config_dict.pop('cert_filename')) | |
key = pathlib.Path(config_dict.pop('key_filename')) | |
return cls(cert_filename=cert, key_filename=key, **config_dict) | |
def update_certs(config: Config): | |
api = ProxmoxAPI( | |
config.api_domain, | |
user=config.user, | |
token_name=config.token_name, | |
token_value=config.token, | |
verify_ssl=config.verify_tls, | |
) | |
for node in config.nodes: | |
log.info('Updating TLS certificate for node %s', node) | |
response = api.nodes(node).certificates.custom.post( | |
certificates=config.cert_filename.read_bytes(), | |
key=config.key_filename.read_bytes(), | |
node=node, | |
force=1, | |
restart=1, | |
) | |
log.debug( | |
'Certificate update API response from node %s: %s', | |
node, | |
response, | |
) | |
def main(): | |
log.info('Starting Proxmox Certupdater.') | |
config_path = os.environ.get('PROXMOX_CERTUPDATER_CONF_FILE', '') | |
if len(sys.argv) > 1: | |
config_path = sys.argv[1] | |
if not config_path: | |
for path in DEFAULT_CONFIG_FILES: | |
if path.is_file(): | |
config_path = path | |
break | |
if not config_path: | |
log.error('Could not find a configuration file to use.') | |
sys.exit(1) | |
log.info('Using configuration from %s', config_path) | |
config = Config.load(config_path) | |
retries = config.retries | |
while retries > 0: | |
retries -= 1 | |
try: | |
update_certs(config) | |
log.info('Successfully updated certs for all nodes with config from %s', config_path) | |
return | |
except requests.exceptions.Timeout as err: | |
log.error('%s: %s', err.__class__.__name__, err) | |
time.sleep(config.retry_delay) | |
log.critical( | |
'Failed to update certificates with config from %s', | |
config_path, | |
) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
proxmoxer | |
requests | |
tomli; python_version < '3.11' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment