Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Acme #132

Merged
merged 10 commits into from
Jan 5, 2025
Merged

Acme #132

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading