Skip to content

Commit

Permalink
Merge pull request #132 from scsitteam/acme
Browse files Browse the repository at this point in the history
Acme
  • Loading branch information
ansibleguy authored Jan 5, 2025
2 parents e761060 + d95dc6a commit 4507e99
Show file tree
Hide file tree
Showing 28 changed files with 3,361 additions and 9 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ not implemented => development => [testing](https://github.com/ansibleguy/collec
### Implemented


| Function | Module | Usage | State |
|:--------------------------|:-----------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------|:---------|
| Function | Module | Usage | State |
|:--------------------------|:-----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|:---------|
| **Base** | ansibleguy.opnsense.list | [Docs](https://opnsense.ansibleguy.net/modules/2_list.html) | stable |
| **Base** | ansibleguy.opnsense.reload | [Docs](https://opnsense.ansibleguy.net/modules/2_reload.html) | stable |
| **Services** | ansibleguy.opnsense.service | [Docs](https://opnsense.ansibleguy.net/modules/service.html) | stable |
Expand Down Expand Up @@ -192,6 +192,11 @@ not implemented => development => [testing](https://github.com/ansibleguy/collec
| **DHCP Relay** | ansibleguy.opnsense.dhcrelay_destination | [Docs](https://opnsense.ansibleguy.net/modules/dhcrelay_destination.html) | unstable |
| **DHCP Reservation** | ansibleguy.opnsense.dhcp_reservation | [Docs](https://opnsense.ansibleguy.net/modules/dhcp.html) | unstable |
| **DHCP Controlagent** | ansibleguy.opnsense.dhcp_controlagent | [Docs](https://opnsense.ansibleguy.net/modules/dhcp.html) | unstable |
| **ACME (Certificates)** | ansibleguy.opnsense.acme_account | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable |
| **ACME (Certificates)** | ansibleguy.opnsense.acme_action | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable |
| **ACME (Certificates)** | ansibleguy.opnsense.acme_general | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable |
| **ACME (Certificates)** | ansibleguy.opnsense.acme_validation | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable |
| **ACME (Certificates)** | ansibleguy.opnsense.acme_certificate | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable |


### Roadmap
Expand Down
507 changes: 507 additions & 0 deletions docs/source/modules/acmeclient.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/source/modules/dhcp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ DHCP
Contribution
************

Thanks to `@KalleDK <https://github.com/KalleDK>`_ for developing these module!
Thanks to `@KalleDK <https://github.com/KalleDK>`_ for developing these modules!

----

Expand Down
2 changes: 1 addition & 1 deletion docs/source/usage/4_develop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ There are `module-templates <https://github.com/ansibleguy/collection_opnsense/b

Rename all calls to the new module.

- Add a cleanup-task in :code:`<COLLECTION>/tests/cleanup.yml` (set state we will expect when re-running the tests)
- Add a cleanup-task in :code:`<COLLECTION>/tests/1_cleanup.yml` (set state we will expect when re-running the tests)

- Enable the test once it runs successfully - add it to :code:`<COLLECTION>/scripts/test.sh`

Expand Down
11 changes: 11 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ action_groups:
dhcp:
- ansibleguy.opnsense.dhcp_reservation
- ansibleguy.opnsense.dhcp_controlagent
acme:
- ansibleguy.opnsense.acme_general
- ansibleguy.opnsense.acme_account
- ansibleguy.opnsense.acme_validation
- ansibleguy.opnsense.acme_action
- ansibleguy.opnsense.acme_certificate
all:
- metadata:
extend_group:
Expand All @@ -150,6 +156,7 @@ action_groups:
- ansibleguy.opnsense.openvpn
- ansibleguy.opnsense.dhcrelay
- ansibleguy.opnsense.dhcp
- ansibleguy.opnsense.acme

plugin_routing:
modules:
Expand Down Expand Up @@ -197,3 +204,7 @@ plugin_routing:
redirect: ansibleguy.opnsense.dhcrelay_destination
unbound_domain:
redirect: ansibleguy.opnsense.unbound_forward
acme_challenge:
redirect: ansibleguy.opnsense.acme_validation
acme_automation:
redirect: ansibleguy.opnsense.acme_action
3 changes: 3 additions & 0 deletions plugins/module_utils/base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ def find(self, match_fields: list) -> None:
self.i.call_cnf['params'] = [match[self.field_pk]]

def process(self) -> None:
self.i.call_cnf['controller'] = self.i.API_CONT
self.i.call_cnf['module'] = self.i.API_MOD

if 'state' in self.i.p and self.i.p['state'] == 'absent':
if self.i.exists:
if hasattr(self.i, 'delete'):
Expand Down
3 changes: 3 additions & 0 deletions plugins/module_utils/base/cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def _base_check(self, match_fields: list = None):
if self.p['state'] == 'present':
self.r['diff']['after'] = self.b.build_diff(data=self.p)

def check(self) -> None:
self._base_check()

def get_existing(self) -> list:
return self.b.get_existing()

Expand Down
58 changes: 58 additions & 0 deletions plugins/module_utils/main/acme_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Account(BaseModule):
FIELD_ID = 'name'
CMDS = {
'add': 'add',
'del': 'del',
'set': 'update',
'search': 'get',
'toggle': 'toggle',
}
API_KEY_PATH = 'acmeclient.accounts.account'
API_MOD = 'acmeclient'
API_CONT = 'accounts'
API_CONT_GET = 'settings'
FIELDS_CHANGE = ['description', 'custom_ca', 'eab_kid', 'eab_hmac']
FIELDS_ALL = [
'enabled', 'name', 'email', 'ca',
]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TYPING = {
'bool': ['enabled'],
'select': ['ca'],
}
EXIST_ATTR = 'account'

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.account = {}

def process(self) -> None:
self.b.process()

if self.p['state'] == 'present' and self.p['register']:
self.register()

def register(self) -> None:
if self.account.get('statusCode', 100) == 200:
return

self.r['changed'] = True
if not self.m.check_mode:
cont_get, mod_get = self.API_CONT, self.API_MOD
self.call_cnf['controller'] = cont_get
self.call_cnf['module'] = mod_get
self.s.post(cnf={
**self.call_cnf,
'command': 'register',
})

def reload(self):
# no reload required
pass
87 changes: 87 additions & 0 deletions plugins/module_utils/main/acme_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \
validate_int_fields, is_unset
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Action(BaseModule):
FIELD_ID = 'name'
CMDS = {
'add': 'add',
'del': 'del',
'set': 'update',
'search': 'get',
'toggle': 'toggle',
}
API_KEY_PATH = 'acmeclient.actions.action'
API_MOD = 'acmeclient'
API_CONT = 'actions'
API_CONT_GET = 'settings'
FIELDS_CHANGE = ['type']
FIELDS_ALL = [
'enabled', 'name', 'description',
# SFTP
'sftp_host', 'sftp_host_key', 'sftp_port', 'sftp_user', 'sftp_identity_type',
'sftp_remote_path', 'sftp_chgrp', 'sftp_chmod', 'sftp_chmod_key',
'sftp_filename_cert', 'sftp_filename_key', 'sftp_filename_ca',
'sftp_filename_fullchain',
# Remote SSH
'remote_ssh_host', 'remote_ssh_host_key', 'remote_ssh_port', 'remote_ssh_user',
'remote_ssh_identity_type', 'remote_ssh_command',
# ACME FRITZ!Box
'acme_fritzbox_url', 'acme_fritzbox_username', 'acme_fritzbox_password',
# ACME PANOS
'acme_panos_username', 'acme_panos_password', 'acme_panos_host',
# ACME promox VE
'acme_proxmoxve_user', 'acme_proxmoxve_server', 'acme_proxmoxve_port',
'acme_proxmoxve_nodename', 'acme_proxmoxve_realm', 'acme_proxmoxve_tokenid',
'acme_proxmoxve_tokenkey',
# ACME Vault
'acme_vault_url', 'acme_vault_prefix', 'acme_vault_token', 'acme_vault_kvv2',
# ACME Synology DSM
'acme_synology_dsm_hostname', 'acme_synology_dsm_port', 'acme_synology_dsm_scheme',
'acme_synology_dsm_username', 'acme_synology_dsm_password', 'acme_synology_dsm_create',
'acme_synology_dsm_deviceid', 'acme_synology_dsm_devicename',
# ACME TrueNAS
'acme_truenas_apikey', 'acme_truenas_hostname', 'acme_truenas_scheme',
# ACME unifi
'acme_unifi_keystore',
]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TYPING = {
'bool': ['enabled', 'acme_vault_kvv2', 'acme_synology_dsm_create'],
'select': [
'type', 'remote_ssh_identity_type', 'acme_synology_dsm_scheme', 'acme_truenas_scheme',
'sftp_identity_type',
],
'int': ['sftp_port', 'remote_ssh_port', 'acme_proxmoxve_port', 'acme_synology_dsm_port'],
}
INT_VALIDATIONS = {
'sftp_port': {'min': 1, 'max': 65535},
}
EXIST_ATTR = 'action'

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.action = {}

def check(self) -> None:
if self.p['state'] == 'present':
if is_unset(self.p['type']):
self.m.fail_json('You need to provide type to create/update actions!')

validate_int_fields(module=self.m, data=self.p, field_minmax=self.INT_VALIDATIONS)

if self.p['type'].startswith('acme_'):
for field in self.FIELDS_ALL:
if field.startswith(self.p['type']) and is_unset(self.p[field]):
self.m.fail_json(f"You need to provide {field} to create/update {self.p['type']} actions!")

self._base_check()

def reload(self):
# no reload required
pass
127 changes: 127 additions & 0 deletions plugins/module_utils/main/acme_certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \
validate_int_fields, is_unset
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Certificate(BaseModule):
FIELD_ID = 'description'
CMDS = {
'add': 'add',
'del': 'del',
'set': 'update',
'search': 'get',
'toggle': 'toggle',
}
API_KEY_PATH = 'acmeclient.certificates.certificate'
API_MOD = 'acmeclient'
API_CONT = 'certificates'
API_CONT_GET = 'settings'
FIELDS_CHANGE = [
'name', 'alt_names', 'account', 'validation', 'restart_actions', 'auto_renewal', 'renew_interval', 'aliasmode'
]
FIELDS_ALL = [
'enabled', 'description', 'domainalias', 'challengealias'
]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TRANSLATE = {
'alt_names': 'altNames',
'validation': 'validationMethod',
'key_ength': 'keyLength',
'restart_actions': 'restartActions',
'auto_renewal': 'autoRenewal',
'renew_interval': 'renewInterval',
}
FIELDS_TYPING = {
'bool': ['enabled', 'auto_renewal'],
'list': ['alt_names', 'restart_actions'],
'select': ['account', 'validation', 'restart_actions', 'aliasmode'],
'int': ['renew_interval'],
}
INT_VALIDATIONS = {
'renew_interval': {'min': 1, 'max': 5000},
}
EXIST_ATTR = 'certificate'
SEARCH_ADDITIONAL = {
'existing_accounts': 'acmeclient.accounts.account',
'existing_validations': 'acmeclient.validations.validation',
'existing_actions': 'acmeclient.actions.action',
}

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.certificate = {}
self.existing_accounts = {}
self.existing_validations = {}
self.existing_actions = {}

def check(self) -> None:
if self.p['state'] == 'present':
if is_unset(self.p['name']):
self.m.fail_json('You need to provide a name to create/update certificates!')

validate_int_fields(module=self.m, data=self.p, field_minmax=self.INT_VALIDATIONS)

if self.p['aliasmode'] == 'domain':
self.FIELDS_CHANGE.append('domainalias')

elif self.p['aliasmode'] == 'challenge':
self.FIELDS_CHANGE.append('challengealias')

self._base_check()

if self.p['state'] == 'present':
self._resolve_relations()

def _resolve_relations(self) -> None:
if is_unset(self.p['account']):
self.m.fail_json('You need to provide an account to create/update certificates!')

else:
if len(self.existing_accounts) > 0:
for key, values in self.existing_accounts.items():
if values['name'] == self.p['account']:
self.p['account'] = key
break

else:
self.m.fail_json(f"Account {self.p['account']} does not exist! {self.existing_accounts}")

if is_unset(self.p['validation']):
self.m.fail_json('You need to provide the validation to create/update certificates!')

else:
if len(self.existing_validations) > 0:
for key, values in self.existing_validations.items():
if values['name'] == self.p['validation']:
self.p['validation'] = key
break

else:
self.m.fail_json(f"Validation {self.p['validation']} does not exist!")

if not is_unset(self.p['restart_actions']):
mapping = {
values['name']: key
for key, values in self.existing_actions.items()
}

missing = [
action
for action in self.p['restart_actions']
if action not in mapping
]
if any(missing):
self.m.fail_json(f"Actions {missing.join(',')} do not exist!")

self.p['restart_actions'] = [
mapping[action]
for action in self.p['restart_actions']
]

def reload(self):
# no reload required
pass
Loading

0 comments on commit 4507e99

Please sign in to comment.