Refactor and extend argument spec helper, use for ACME modules (#749)

* Refactor argument spec helper.

* Remove superfluous comments.
pull/743/head
Felix Fontein 2024-05-05 11:42:42 +02:00 committed by Austin Lucas Lake
parent f9f22311e4
commit 044a3be9e0
No known key found for this signature in database
GPG Key ID: 6A37FA54CFCFA4DB
16 changed files with 166 additions and 121 deletions

View File

@ -0,0 +1,2 @@
deprecated_features:
- "crypto.module_backends.common module utils - the ``crypto.module_backends.common`` module utils is deprecated and will be removed from community.crypto 3.0.0. Use the improved ``argspec`` module util instead (https://github.com/ansible-collections/community.crypto/pull/749)."

View File

@ -21,6 +21,8 @@ from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six import PY3 from ansible.module_utils.six import PY3
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import ( from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
OpenSSLCLIBackend, OpenSSLCLIBackend,
) )
@ -439,6 +441,28 @@ def get_default_argspec(with_account=True):
return argspec return argspec
def create_default_argspec(with_account=True, require_account_key=True):
'''
Provides default argument spec for the options documented in the acme doc fragment.
'''
result = ArgumentSpec(
get_default_argspec(with_account=with_account),
)
if with_account:
if require_account_key:
result.update(
required_one_of=[
['account_key_src', 'account_key_content'],
],
)
result.update(
mutually_exclusive=[
['account_key_src', 'account_key_content'],
],
)
return result
def create_backend(module, needs_acme_v2): def create_backend(module, needs_acme_v2):
if not HAS_IPADDRESS: if not HAS_IPADDRESS:
module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR) module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
def _ensure_list(value):
if value is None:
return []
return list(value)
class ArgumentSpec:
def __init__(self, argument_spec=None, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
self.argument_spec = argument_spec or {}
self.mutually_exclusive = _ensure_list(mutually_exclusive)
self.required_together = _ensure_list(required_together)
self.required_one_of = _ensure_list(required_one_of)
self.required_if = _ensure_list(required_if)
self.required_by = required_by or {}
def update_argspec(self, **kwargs):
self.argument_spec.update(kwargs)
return self
def update(self, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
if mutually_exclusive:
self.mutually_exclusive.extend(mutually_exclusive)
if required_together:
self.required_together.extend(required_together)
if required_one_of:
self.required_one_of.extend(required_one_of)
if required_if:
self.required_if.extend(required_if)
if required_by:
for k, v in required_by.items():
if k in self.required_by:
v = list(self.required_by[k]) + list(v)
self.required_by[k] = v
return self
def merge(self, other):
self.update_argspec(other.argument_spec)
self.update(
mutually_exclusive=other.mutually_exclusive,
required_together=other.required_together,
required_one_of=other.required_one_of,
required_if=other.required_if,
required_by=other.required_by,
)
return self
def create_ansible_module_helper(self, clazz, args, **kwargs):
return clazz(
*args,
argument_spec=self.argument_spec,
mutually_exclusive=self.mutually_exclusive,
required_together=self.required_together,
required_one_of=self.required_one_of,
required_if=self.required_if,
required_by=self.required_by,
**kwargs)
def create_ansible_module(self, **kwargs):
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
__all__ = ('ArgumentSpec', )

View File

@ -15,9 +15,9 @@ import traceback
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError, OpenSSLObjectError,

View File

@ -10,26 +10,19 @@ __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec as _ArgumentSpec
class ArgumentSpec:
def __init__(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
self.argument_spec = argument_spec
self.mutually_exclusive = mutually_exclusive or []
self.required_together = required_together or []
self.required_one_of = required_one_of or []
self.required_if = required_if or []
self.required_by = required_by or {}
class ArgumentSpec(_ArgumentSpec):
def create_ansible_module_helper(self, clazz, args, **kwargs): def create_ansible_module_helper(self, clazz, args, **kwargs):
return clazz( result = super(ArgumentSpec, self).create_ansible_module_helper(clazz, args, **kwargs)
*args, result.deprecate(
argument_spec=self.argument_spec, "The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0."
mutually_exclusive=self.mutually_exclusive, " Use the argspec module utils from community.crypto instead.",
required_together=self.required_together, version='3.0.0',
required_one_of=self.required_one_of, collection_name='community.crypto',
required_if=self.required_if, )
required_by=self.required_by, return result
**kwargs)
def create_ansible_module(self, **kwargs):
return self.create_ansible_module_helper(AnsibleModule, (), **kwargs) __all__ = ('AnsibleModule', 'ArgumentSpec')

View File

@ -17,6 +17,8 @@ from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.common.text.converters import to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@ -49,8 +51,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_csr_info, get_csr_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'

View File

@ -17,6 +17,8 @@ from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@ -42,8 +44,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
get_privatekey_info, get_privatekey_info,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'

View File

@ -15,12 +15,14 @@ from ansible.module_utils import six
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion from ansible_collections.community.crypto.plugins.module_utils.argspec import ArgumentSpec
from ansible_collections.community.crypto.plugins.module_utils.io import ( from ansible_collections.community.crypto.plugins.module_utils.io import (
load_file, load_file,
) )
from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
CRYPTOGRAPHY_HAS_X25519, CRYPTOGRAPHY_HAS_X25519,
CRYPTOGRAPHY_HAS_X448, CRYPTOGRAPHY_HAS_X448,
@ -37,8 +39,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
identify_private_key_format, identify_private_key_format,
) )
from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'

View File

@ -170,11 +170,9 @@ account_uri:
import base64 import base64
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -189,8 +187,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec()
argument_spec.update(dict( argument_spec.update_argspec(
terms_agreed=dict(type='bool', default=False), terms_agreed=dict(type='bool', default=False),
state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']),
allow_creation=dict(type='bool', default=True), allow_creation=dict(type='bool', default=True),
@ -203,14 +201,9 @@ def main():
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']), alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
key=dict(type='str', required=True, no_log=True), key=dict(type='str', required=True, no_log=True),
)) ))
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
),
mutually_exclusive=( mutually_exclusive=(
['account_key_src', 'account_key_content'],
['new_account_key_src', 'new_account_key_content'], ['new_account_key_src', 'new_account_key_content'],
), ),
required_if=( required_if=(
@ -218,8 +211,8 @@ def main():
# new_account_key_src and new_account_key_content are specified # new_account_key_src and new_account_key_content are specified
['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True],
), ),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
if module.params['external_account_binding']: if module.params['external_account_binding']:

View File

@ -214,11 +214,9 @@ order_uris:
version_added: 1.5.0 version_added: 1.5.0
''' '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -271,20 +269,11 @@ def get_order(client, order_url):
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec()
argument_spec.update(dict( argument_spec.update_argspec(
retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
try: try:

View File

@ -98,11 +98,9 @@ renewal_info:
sample: '2024-04-29T01:17:10.236921+00:00' sample: '2024-04-29T01:17:10.236921+00:00'
''' '''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -110,21 +108,20 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main(): def main():
argument_spec = get_default_argspec(with_account=False) argument_spec = create_default_argspec(with_account=False)
argument_spec.update(dict( argument_spec.update_argspec(
certificate_path=dict(type='path'), certificate_path=dict(type='path'),
certificate_content=dict(type='str'), certificate_content=dict(type='str'),
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
required_one_of=( required_one_of=(
['certificate_path', 'certificate_content'], ['certificate_path', 'certificate_content'],
), ),
mutually_exclusive=( mutually_exclusive=(
['certificate_path', 'certificate_content'], ['certificate_path', 'certificate_content'],
), ),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
try: try:

View File

@ -592,11 +592,9 @@ all_chains:
import os import os
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -922,8 +920,8 @@ class ACMECertificateClient(object):
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec()
argument_spec.update(dict( argument_spec.update_argspec(
modify_account=dict(type='bool', default=True), modify_account=dict(type='bool', default=True),
account_email=dict(type='str'), account_email=dict(type='str'),
agreement=dict(type='str'), agreement=dict(type='str'),
@ -947,20 +945,17 @@ def main():
authority_key_identifier=dict(type='str'), authority_key_identifier=dict(type='str'),
)), )),
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'), include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
required_one_of=( required_one_of=(
['account_key_src', 'account_key_content'],
['dest', 'fullchain_dest'], ['dest', 'fullchain_dest'],
['csr', 'csr_content'], ['csr', 'csr_content'],
), ),
mutually_exclusive=( mutually_exclusive=(
['account_key_src', 'account_key_content'],
['csr', 'csr_content'], ['csr', 'csr_content'],
), ),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, False) backend = create_backend(module, False)
try: try:

View File

@ -54,11 +54,9 @@ EXAMPLES = r'''
RETURN = '''#''' RETURN = '''#'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -76,20 +74,11 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.orders impor
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec()
argument_spec.update(dict( argument_spec.update_argspec(
order_uri=dict(type='str', required=True), order_uri=dict(type='str', required=True),
))
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=(
['account_key_src', 'account_key_content'],
),
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
if module.params['acme_version'] == 1: if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1') module.fail_json('The module does not support acme_version=1')

View File

@ -131,11 +131,9 @@ cert_id:
import os import os
import random import random
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -145,8 +143,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def main(): def main():
argument_spec = get_default_argspec(with_account=False) argument_spec = create_default_argspec(with_account=False)
argument_spec.update(dict( argument_spec.update_argspec(
certificate_path=dict(type='path'), certificate_path=dict(type='path'),
certificate_content=dict(type='str'), certificate_content=dict(type='str'),
use_ari=dict(type='bool', default=True), use_ari=dict(type='bool', default=True),
@ -154,14 +152,13 @@ def main():
remaining_days=dict(type='int'), remaining_days=dict(type='int'),
remaining_percentage=dict(type='float'), remaining_percentage=dict(type='float'),
now=dict(type='str'), now=dict(type='str'),
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
mutually_exclusive=( mutually_exclusive=(
['certificate_path', 'certificate_content'], ['certificate_path', 'certificate_content'],
), ),
supports_check_mode=True,
) )
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True) backend = create_backend(module, True)
result = dict( result = dict(
@ -223,13 +220,11 @@ def main():
), ),
) )
# TODO check remaining_days
if module.params['remaining_days'] is not None: if module.params['remaining_days'] is not None:
remaining_days = (cert_info.not_valid_after - now).days remaining_days = (cert_info.not_valid_after - now).days
if remaining_days < module.params['remaining_days']: if remaining_days < module.params['remaining_days']:
complete(True, msg='The certificate expires in {0} days'.format(remaining_days)) complete(True, msg='The certificate expires in {0} days'.format(remaining_days))
# TODO check remaining_percentage
if module.params['remaining_percentage'] is not None: if module.params['remaining_percentage'] is not None:
timestamp = backend.interpolate_timestamp(cert_info.not_valid_before, cert_info.not_valid_after, 1 - module.params['remaining_percentage']) timestamp = backend.interpolate_timestamp(cert_info.not_valid_before, cert_info.not_valid_after, 1 - module.params['remaining_percentage'])
if timestamp < now: if timestamp < now:

View File

@ -128,11 +128,9 @@ EXAMPLES = '''
RETURN = '''#''' RETURN = '''#'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -153,24 +151,23 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update(dict( argument_spec.update_argspec(
private_key_src=dict(type='path'), private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True), private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True), private_key_passphrase=dict(type='str', no_log=True),
certificate=dict(type='path', required=True), certificate=dict(type='path', required=True),
revoke_reason=dict(type='int'), revoke_reason=dict(type='int'),
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
required_one_of=( required_one_of=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
), ),
mutually_exclusive=( mutually_exclusive=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
), ),
supports_check_mode=False,
) )
module = argument_spec.create_ansible_module()
backend = create_backend(module, False) backend = create_backend(module, False)
try: try:

View File

@ -248,12 +248,11 @@ output_json:
- ... - ...
''' '''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend, create_backend,
get_default_argspec, create_default_argspec,
ACMEClient, ACMEClient,
) )
@ -264,18 +263,14 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main(): def main():
argument_spec = get_default_argspec() argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update(dict( argument_spec.update_argspec(
url=dict(type='str'), url=dict(type='str'),
method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'), method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'),
content=dict(type='str'), content=dict(type='str'),
fail_on_acme_error=dict(type='bool', default=True), fail_on_acme_error=dict(type='bool', default=True),
)) )
module = AnsibleModule( argument_spec.update(
argument_spec=argument_spec,
mutually_exclusive=(
['account_key_src', 'account_key_content'],
),
required_if=( required_if=(
['method', 'get', ['url']], ['method', 'get', ['url']],
['method', 'post', ['url', 'content']], ['method', 'post', ['url', 'content']],
@ -283,6 +278,7 @@ def main():
['method', 'post', ['account_key_src', 'account_key_content'], True], ['method', 'post', ['account_key_src', 'account_key_content'], True],
), ),
) )
module = argument_spec.create_ansible_module()
backend = create_backend(module, False) backend = create_backend(module, False)
result = dict() result = dict()