Skip to content

Commit

Permalink
Merge pull request #55589 from ogd-software/acme_state_fixup-master
Browse files Browse the repository at this point in the history
Acme state fixup master
  • Loading branch information
dwoz authored Dec 16, 2019
2 parents 06e0d9c + 4beb9d3 commit 21ed973
Show file tree
Hide file tree
Showing 8 changed files with 1,105 additions and 127 deletions.
131 changes: 131 additions & 0 deletions doc/topics/jinja/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,137 @@ Returns:
d94a45acd81f8e3107d237dbc0d5d195f6a52a0d188bc0284c0763ece1eac9f9496fb6a531a296074c87b3540398dace1222b42e150e67c9301383fde3d66ae5
.. jinja_ref:: set_dict_key_value

``set_dict_key_value``
----------------------

..versionadded:: Neon

Allows you to set a value in a nested dictionary without having to worry if all the nested keys actually exist.
Missing keys will be automatically created if they do not exist.
The default delimiter for the keys is ':', however, with the `delimiter`-parameter, a different delimiter can be specified.

Examples:

.. code-block:: jinja
Example 1:
{%- set foo = {} %}
{{ foo | set_dict_key_value('bar:baz', 42) }}

Example 2:
{{ {} | set_dict_key_value('bar.baz.qux', 42, delimiter='.') }}

Returns:

.. code-block:: text
Example 1:
{'bar': {'baz': 42}}

Example 2:
{'bar': {'baz': {'qux': 42}}}


.. jinja_ref:: append_dict_key_value

``append_dict_key_value``
-------------------------

..versionadded:: Neon

Allows you to append to a list nested (deep) in a dictionary without having to worry if all the nested keys (or the list itself) actually exist.
Missing keys will automatically be created if they do not exist.
The default delimiter for the keys is ':', however, with the `delimiter`-parameter, a different delimiter can be specified.

Examples:

.. code-block:: jinja
Example 1:
{%- set foo = {'bar': {'baz': [1, 2]}} %}
{{ foo | append_dict_key_value('bar:baz', 42) }}

Example 2:
{%- set foo = {} %}
{{ foo | append_dict_key_value('bar:baz:qux', 42) }}

Returns:

.. code-block:: text
Example 1:
{'bar': {'baz': [1, 2, 42]}}

Example 2:
{'bar': {'baz': {'qux': [42]}}}


.. jinja_ref:: extend_dict_key_value

``extend_dict_key_value``
-------------------------

..versionadded:: Neon

Allows you to extend a list nested (deep) in a dictionary without having to worry if all the nested keys (or the list itself) actually exist.
Missing keys will automatically be created if they do not exist.
The default delimiter for the keys is ':', however, with the `delimiter`-parameter, a different delimiter can be specified.

Examples:

.. code-block:: jinja
Example 1:
{%- set foo = {'bar': {'baz': [1, 2]}} %}
{{ foo | extend_dict_key_value('bar:baz', [42, 42]) }}

Example 2:
{{ {} | extend_dict_key_value('bar:baz:qux', [42]) }}

Returns:

.. code-block:: text
Example 1:
{'bar': {'baz': [1, 2, 42, 42]}}

Example 2:
{'bar': {'baz': {'qux': [42]}}}


.. jinja_ref:: update_dict_key_value

``update_dict_key_value``
-------------------------

..versionadded:: Neon

Allows you to update a dictionary nested (deep) in another dictionary without having to worry if all the nested keys actually exist.
Missing keys will automatically be created if they do not exist.
The default delimiter for the keys is ':', however, with the `delimiter`-parameter, a different delimiter can be specified.

Examples:

.. code-block:: jinja
Example 1:
{%- set foo = {'bar': {'baz': {'qux': 1}}} %}
{{ foo | update_dict_key_value('bar:baz', {'quux': 3}) }}

Example 2:
{{ {} | update_dict_key_value('bar:baz:qux', {'quux': 3}) }}

.. code-block:: text
Example 1:
{'bar': {'baz': {'qux': 1, 'quux': 3}}}

Example 2:
{'bar': {'baz': {'qux': {'quux': 3}}}}


.. jinja_ref:: md5

``md5``
Expand Down
114 changes: 75 additions & 39 deletions salt/modules/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

# Import salt libs
import salt.utils.path
from salt.exceptions import SaltInvocationError

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -69,30 +70,31 @@ def _expires(name):
'''
Return the expiry date of a cert
:return datetime object of expiry date
:rtype: datetime
:return: Expiry date
'''
cert_file = _cert_file(name, 'cert')
# Use the salt module if available
if 'tls.cert_info' in __salt__:
expiry = __salt__['tls.cert_info'](cert_file)['not_after']
expiry = __salt__['tls.cert_info'](cert_file).get('not_after', 0)
# Cobble it together using the openssl binary
else:
openssl_cmd = 'openssl x509 -in {0} -noout -enddate'.format(cert_file)
# No %e format on my Linux'es here
strptime_sux_cmd = 'date --date="$({0} | cut -d= -f2)" +%s'.format(openssl_cmd)
expiry = float(__salt__['cmd.shell'](strptime_sux_cmd, output_loglevel='quiet'))
# expiry = datetime.datetime.strptime(expiry.split('=', 1)[-1], '%b %e %H:%M:%S %Y %Z')

return datetime.datetime.fromtimestamp(expiry)


def _renew_by(name, window=None):
'''
Date before a certificate should be renewed
:param name: Common Name of the certificate (DNS name of certificate)
:param window: days before expiry date to renew
:return datetime object of first renewal date
:param str name: Common Name of the certificate (DNS name of certificate)
:param int window: days before expiry date to renew
:rtype: datetime
:return: First renewal date
'''
expiry = _expires(name)
if window is not None:
Expand Down Expand Up @@ -127,35 +129,44 @@ def cert(name,
:param aliases: subjectAltNames (Additional DNS names on certificate)
:param email: e-mail address for interaction with ACME provider
:param webroot: True or a full path to use to use webroot. Otherwise use standalone mode
:param test_cert: Request a certificate from the Happy Hacker Fake CA (mutually exclusive with 'server')
:param renew: True/'force' to force a renewal, or a window of renewal before expiry in days
:param test_cert: Request a certificate from the Happy Hacker Fake CA (mutually
exclusive with 'server')
:param renew: True/'force' to force a renewal, or a window of renewal before
expiry in days
:param keysize: RSA key bits
:param server: API endpoint to talk to
:param owner: owner of the private key file
:param group: group of the private key file
:param mode: mode of the private key file
:param certname: Name of the certificate to save
:param preferred_challenges: A sorted, comma delimited list of the preferred
challenge to use during authorization with the
most preferred challenge listed first.
challenge to use during authorization with the most preferred challenge
listed first.
:param tls_sni_01_port: Port used during tls-sni-01 challenge. This only affects
the port Certbot listens on. A conforming ACME server
will still attempt to connect on port 443.
the port Certbot listens on. A conforming ACME server will still attempt
to connect on port 443.
:param tls_sni_01_address: The address the server listens to during tls-sni-01
challenge.
challenge.
:param http_01_port: Port used in the http-01 challenge. This only affects
the port Certbot listens on. A conforming ACME server
will still attempt to connect on port 80.
the port Certbot listens on. A conforming ACME server will still attempt
to connect on port 80.
:param https_01_address: The address the server listens to during http-01 challenge.
:param dns_plugin: Name of a DNS plugin to use (currently only 'cloudflare')
:param dns_plugin_credentials: Path to the credentials file if required by the specified DNS plugin
:return: dict with 'result' True/False/None, 'comment' and certificate's expiry date ('not_after')
:param dns_plugin: Name of a DNS plugin to use (currently only 'cloudflare'
or 'digitalocean')
:param dns_plugin_credentials: Path to the credentials file if required by
the specified DNS plugin
:param dns_plugin_propagate_seconds: Number of seconds to wait for DNS propogations
before asking ACME servers to verify the DNS record. (default 10)
:rtype: dict
:return: Dictionary with 'result' True/False/None, 'comment' and certificate's
expiry date ('not_after')
CLI example:
.. code-block:: bash
salt 'gitlab.example.com' acme.cert dev.example.com "[gitlab.example.com]" test_cert=True renew=14 webroot=/opt/gitlab/embedded/service/gitlab-rails/public
salt 'gitlab.example.com' acme.cert dev.example.com "[gitlab.example.com]" test_cert=True \
renew=14 webroot=/opt/gitlab/embedded/service/gitlab-rails/public
'''

cmd = [LEA, 'certonly', '--non-interactive', '--agree-tos']
Expand Down Expand Up @@ -224,9 +235,13 @@ def cert(name,
cmd.append('--expand')
res = __salt__['cmd.run_all'](' '.join(cmd))
if res['retcode'] != 0:
return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])}
return {'result': False,
'comment': ('Certificate {0} renewal failed with:\n{1}'
''.format(name, res['stderr']))}
else:
return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])}
return {'result': False,
'comment': ('Certificate {0} renewal failed with:\n{1}'
''.format(name, res['stderr']))}

if 'no action taken' in res['stdout']:
comment = 'Certificate {0} unchanged'.format(cert_file)
Expand Down Expand Up @@ -257,43 +272,52 @@ def certs():
salt 'vhost.example.com' acme.certs
'''
return __salt__['file.readdir'](LE_LIVE)[2:]
return [item for item in __salt__['file.readdir'](LE_LIVE)[2:] if os.path.isdir(item)]


def info(name):
'''
Return information about a certificate
.. note::
Will output tls.cert_info if that's available, or OpenSSL text if not
:param name: CommonName of cert
:param str name: CommonName of certificate
:rtype: dict
:return: Dictionary with information about the certificate.
If neither the ``tls`` nor the ``x509`` module can be used to determine
the certificate information, the information will be retrieved as one
big text block under the key ``text`` using the openssl cli.
CLI example:
.. code-block:: bash
salt 'gitlab.example.com' acme.info dev.example.com
'''
if not has(name):
return {}
cert_file = _cert_file(name, 'cert')
# Use the salt module if available
# Use the tls salt module if available
if 'tls.cert_info' in __salt__:
cert_info = __salt__['tls.cert_info'](cert_file)
# Strip out the extensions object contents;
# these trip over our poor state output
# and they serve no real purpose here anyway
cert_info['extensions'] = cert_info['extensions'].keys()
return cert_info
# Cobble it together using the openssl binary
openssl_cmd = 'openssl x509 -in {0} -noout -text'.format(cert_file)
return __salt__['cmd.run'](openssl_cmd, output_loglevel='quiet')
elif 'x509.read_certificate' in __salt__:
cert_info = __salt__['x509.read_certificate'](cert_file)
else:
# Cobble it together using the openssl binary
openssl_cmd = 'openssl x509 -in {0} -noout -text'.format(cert_file)
cert_info = {'text': __salt__['cmd.run'](openssl_cmd, output_loglevel='quiet')}
return cert_info


def expires(name):
'''
The expiry date of a certificate in ISO format
:param name: CommonName of cert
:param str name: CommonName of certificate
:rtype: str
:return: Expiry date in ISO format.
CLI example:
Expand All @@ -308,7 +332,8 @@ def has(name):
'''
Test if a certificate is in the Let's Encrypt Live directory
:param name: CommonName of cert
:param str name: CommonName of certificate
:rtype: bool
Code example:
Expand All @@ -324,8 +349,10 @@ def renew_by(name, window=None):
'''
Date in ISO format when a certificate should first be renewed
:param name: CommonName of cert
:param window: number of days before expiry when renewal should take place
:param str name: CommonName of certificate
:param int window: number of days before expiry when renewal should take place
:rtype: str
:return: Date of certificate renewal in ISO format.
'''
return _renew_by(name, window).isoformat()

Expand All @@ -334,8 +361,10 @@ def needs_renewal(name, window=None):
'''
Check if a certificate needs renewal
:param name: CommonName of cert
:param window: Window in days to renew earlier or True/force to just return True
:param str name: CommonName of certificate
:param bool/str/int window: Window in days to renew earlier or True/force to just return True
:rtype: bool
:return: Whether or not the certificate needs to be renewed.
Code example:
Expand All @@ -346,7 +375,14 @@ def needs_renewal(name, window=None):
else:
log.info('Your certificate is still good')
'''
if window is not None and window in ('force', 'Force', True):
return True
if window:
if str(window).lower in ('force', 'true'):
return True
if not (isinstance(window, int) or (hasattr(window, 'isdigit') and window.isdigit())):
raise SaltInvocationError(
'The argument "window", if provided, must be one of the following : '
'True (boolean), "force" or "Force" (str) or a numerical value in days.'
)
window = int(window)

return _renew_by(name, window) <= datetime.datetime.today()
Loading

0 comments on commit 21ed973

Please sign in to comment.