Skip to content

Instantly share code, notes, and snippets.

@haxwithaxe
Last active October 21, 2023 23:08
Show Gist options
  • Save haxwithaxe/1aa59ed9049e878c1b59a671b56d4c5b to your computer and use it in GitHub Desktop.
Save haxwithaxe/1aa59ed9049e878c1b59a671b56d4c5b to your computer and use it in GitHub Desktop.
A certdeploy compatible script to update certs in a proxmox cluster.
#!/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()
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