Improve CI (#281)

* Install PyOpenSSL and cryptography from PyPi if target Python != system Python.

* Work around some CentOS6, 7, Ubuntu 16.04 problems. Improve jinja2 compatibility handling.

* Skip tasks that require properties that aren't always there.

* Only install OpenSSL when not present.

* Improve output.

* Improve get_certificate integration test graceful failing.

* Fix tests.

* Fix assert.

* OpenSSL peculiarities.

* Fix condition.
pull/267/head
Felix Fontein 2021-09-18 15:21:40 +02:00 committed by GitHub
parent 63f4598737
commit 6c018b94da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 529 additions and 172 deletions

View File

@ -2,8 +2,8 @@
- name: Generate account keys - name: Generate account keys
openssl_privatekey: openssl_privatekey:
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
passphrase: "{{ item.pass | default(omit, true) }}" passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
cipher: "{{ 'auto' if item.pass | default() else omit }}" cipher: "{{ 'auto' if (item.pass | default(false)) else omit }}"
type: ECC type: ECC
curve: secp256r1 curve: secp256r1
force: true force: true
@ -12,7 +12,7 @@
- name: Parse account keys (to ease debugging some test failures) - name: Parse account keys (to ease debugging some test failures)
openssl_privatekey_info: openssl_privatekey_info:
path: "{{ remote_tmp_dir }}/{{ item.name }}.pem" path: "{{ remote_tmp_dir }}/{{ item.name }}.pem"
passphrase: "{{ item.pass | default(omit, true) }}" passphrase: "{{ item.pass | default(omit) | default(omit, true) }}"
return_private_key_data: true return_private_key_data: true
loop: "{{ account_keys }}" loop: "{{ account_keys }}"

View File

@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_acme - setup_acme
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -264,92 +264,98 @@
set_fact: set_fact:
cert_5_recreate_3: "{{ challenge_data is changed }}" cert_5_recreate_3: "{{ challenge_data is changed }}"
cert_5d_obtain_results: "{{ certificate_obtain_result }}" cert_5d_obtain_results: "{{ certificate_obtain_result }}"
- name: Obtain cert 6 - block:
include_tasks: obtain-cert.yml - name: Obtain cert 6
vars: include_tasks: obtain-cert.yml
certgen_title: Certificate 6 vars:
certificate_name: cert-6 certgen_title: Certificate 6
key_type: rsa certificate_name: cert-6
rsa_bits: "{{ default_rsa_key_size }}" key_type: rsa
subject_alt_name: "DNS:example.org" rsa_bits: "{{ default_rsa_key_size }}"
subject_alt_name_critical: no subject_alt_name: "DNS:example.org"
account_key: account-ec256 subject_alt_name_critical: no
challenge: tls-alpn-01 account_key: account-ec256
modify_account: yes challenge: tls-alpn-01
deactivate_authzs: no modify_account: yes
force: no deactivate_authzs: no
remaining_days: 10 force: no
terms_agreed: yes remaining_days: 10
account_email: "example@example.org" terms_agreed: yes
acme_expected_root_number: 0 account_email: "example@example.org"
select_chain: acme_expected_root_number: 0
# All intermediates have the same subject key identifier, so always select_chain:
# the first chain will be found, and we need a second condition to # All intermediates have the same subject key identifier, so always
# make sure that the first condition actually works. (The second # the first chain will be found, and we need a second condition to
# condition has been tested above.) # make sure that the first condition actually works. (The second
- test_certificates: first # condition has been tested above.)
subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}" - test_certificates: first
- test_certificates: last subject_key_identifier: "{{ acme_intermediates[0].subject_key_identifier }}"
issuer: "{{ acme_roots[1].subject }}" - test_certificates: last
use_csr_content: true issuer: "{{ acme_roots[1].subject }}"
- name: Store obtain results for cert 6 use_csr_content: true
set_fact: - name: Store obtain results for cert 6
cert_6_obtain_results: "{{ certificate_obtain_result }}" set_fact:
cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" cert_6_obtain_results: "{{ certificate_obtain_result }}"
- name: Obtain cert 7 cert_6_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
include_tasks: obtain-cert.yml when: acme_intermediates[0].subject_key_identifier is defined
vars: - block:
certgen_title: Certificate 7 - name: Obtain cert 7
certificate_name: cert-7 include_tasks: obtain-cert.yml
key_type: rsa vars:
rsa_bits: "{{ default_rsa_key_size }}" certgen_title: Certificate 7
subject_alt_name: certificate_name: cert-7
- "IP:127.0.0.1" key_type: rsa
# - "IP:::1" rsa_bits: "{{ default_rsa_key_size }}"
subject_alt_name_critical: no subject_alt_name:
account_key: account-ec256 - "IP:127.0.0.1"
challenge: http-01 # - "IP:::1"
modify_account: yes subject_alt_name_critical: no
deactivate_authzs: no account_key: account-ec256
force: no challenge: http-01
remaining_days: 10 modify_account: yes
terms_agreed: yes deactivate_authzs: no
account_email: "example@example.org" force: no
acme_expected_root_number: 2 remaining_days: 10
select_chain: terms_agreed: yes
- test_certificates: last account_email: "example@example.org"
authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}" acme_expected_root_number: 2
use_csr_content: false select_chain:
- name: Store obtain results for cert 7 - test_certificates: last
set_fact: authority_key_identifier: "{{ acme_roots[2].subject_key_identifier }}"
cert_7_obtain_results: "{{ certificate_obtain_result }}" use_csr_content: false
cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}" - name: Store obtain results for cert 7
- name: Obtain cert 8 set_fact:
include_tasks: obtain-cert.yml cert_7_obtain_results: "{{ certificate_obtain_result }}"
vars: cert_7_alternate: "{{ 2 if select_crypto_backend == 'cryptography' else 0 }}"
certgen_title: Certificate 8 when: acme_roots[2].subject_key_identifier is defined
certificate_name: cert-8 - block:
key_type: rsa - name: Obtain cert 8
rsa_bits: "{{ default_rsa_key_size }}" include_tasks: obtain-cert.yml
subject_alt_name: vars:
- "IP:127.0.0.1" certgen_title: Certificate 8
# IPv4 only since our test validation server doesn't work certificate_name: cert-8
# with IPv6 (thanks to Python's socketserver). key_type: rsa
subject_alt_name_critical: no rsa_bits: "{{ default_rsa_key_size }}"
account_key: account-ec256 subject_alt_name:
challenge: tls-alpn-01 - "IP:127.0.0.1"
challenge_alpn_tls: acme_challenge_cert_helper # IPv4 only since our test validation server doesn't work
modify_account: yes # with IPv6 (thanks to Python's socketserver).
deactivate_authzs: no subject_alt_name_critical: no
force: no account_key: account-ec256
remaining_days: 10 challenge: tls-alpn-01
terms_agreed: yes challenge_alpn_tls: acme_challenge_cert_helper
account_email: "example@example.org" modify_account: yes
use_csr_content: true deactivate_authzs: no
- name: Store obtain results for cert 8 force: no
set_fact: remaining_days: 10
cert_8_obtain_results: "{{ certificate_obtain_result }}" terms_agreed: yes
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}" account_email: "example@example.org"
use_csr_content: true
- name: Store obtain results for cert 8
set_fact:
cert_8_obtain_results: "{{ certificate_obtain_result }}"
cert_8_alternate: "{{ 0 if select_crypto_backend == 'cryptography' else 0 }}"
when: cryptography_version.stdout is version('1.3', '>=')
## DISSECT CERTIFICATES ####################################################################### ## DISSECT CERTIFICATES #######################################################################
# Make sure certificates are valid. Root certificate for Pebble equals the chain certificate. # Make sure certificates are valid. Root certificate for Pebble equals the chain certificate.
- name: Verifying cert 1 - name: Verifying cert 1
@ -376,14 +382,17 @@
command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-6-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-6-chain.pem" "{{ remote_tmp_dir }}/cert-6.pem"' command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-6-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-6-chain.pem" "{{ remote_tmp_dir }}/cert-6.pem"'
ignore_errors: yes ignore_errors: yes
register: cert_6_valid register: cert_6_valid
when: acme_intermediates[0].subject_key_identifier is defined
- name: Verifying cert 7 - name: Verifying cert 7
command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-7-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-7-chain.pem" "{{ remote_tmp_dir }}/cert-7.pem"' command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-7-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-7-chain.pem" "{{ remote_tmp_dir }}/cert-7.pem"'
ignore_errors: yes ignore_errors: yes
register: cert_7_valid register: cert_7_valid
when: acme_roots[2].subject_key_identifier is defined
- name: Verifying cert 8 - name: Verifying cert 8
command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-8-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-8-chain.pem" "{{ remote_tmp_dir }}/cert-8.pem"' command: '{{ openssl_binary }} verify -CAfile "{{ remote_tmp_dir }}/cert-8-root.pem" -untrusted "{{ remote_tmp_dir }}/cert-8-chain.pem" "{{ remote_tmp_dir }}/cert-8.pem"'
ignore_errors: yes ignore_errors: yes
register: cert_8_valid register: cert_8_valid
when: cryptography_version.stdout is version('1.3', '>=')
# Dump certificate info # Dump certificate info
- name: Dumping cert 1 - name: Dumping cert 1
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-1.pem" -noout -text' command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-1.pem" -noout -text'
@ -403,12 +412,15 @@
- name: Dumping cert 6 - name: Dumping cert 6
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-6.pem" -noout -text' command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-6.pem" -noout -text'
register: cert_6_text register: cert_6_text
when: acme_intermediates[0].subject_key_identifier is defined
- name: Dumping cert 7 - name: Dumping cert 7
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-7.pem" -noout -text' command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-7.pem" -noout -text'
register: cert_7_text register: cert_7_text
when: acme_roots[2].subject_key_identifier is defined
- name: Dumping cert 8 - name: Dumping cert 8
command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-8.pem" -noout -text' command: '{{ openssl_binary }} x509 -in "{{ remote_tmp_dir }}/cert-8.pem" -noout -text'
register: cert_8_text register: cert_8_text
when: cryptography_version.stdout is version('1.3', '>=')
# Dump certificate info # Dump certificate info
- name: Dumping cert 1 - name: Dumping cert 1
x509_certificate_info: x509_certificate_info:
@ -434,14 +446,17 @@
x509_certificate_info: x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-6.pem" path: "{{ remote_tmp_dir }}/cert-6.pem"
register: cert_6_info register: cert_6_info
when: acme_intermediates[0].subject_key_identifier is defined
- name: Dumping cert 7 - name: Dumping cert 7
x509_certificate_info: x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-7.pem" path: "{{ remote_tmp_dir }}/cert-7.pem"
register: cert_7_info register: cert_7_info
when: acme_roots[2].subject_key_identifier is defined
- name: Dumping cert 8 - name: Dumping cert 8
x509_certificate_info: x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-8.pem" path: "{{ remote_tmp_dir }}/cert-8.pem"
register: cert_8_info register: cert_8_info
when: cryptography_version.stdout is version('1.3', '>=')
## GET ACCOUNT ORDERS ######################################################################### ## GET ACCOUNT ORDERS #########################################################################
- name: Don't retrieve orders - name: Don't retrieve orders
acme_account_info: acme_account_info:

View File

@ -124,14 +124,38 @@
that: that:
- cert_5_recreate_3 == True - cert_5_recreate_3 == True
- name: Check that certificate 6 is valid - block:
assert: - name: Check that certificate 6 is valid
that: assert:
- cert_6_valid is not failed that:
- name: Check that certificate 6 contains correct SANs - cert_6_valid is not failed
assert: - name: Check that certificate 6 contains correct SANs
that: assert:
- "'DNS:example.org' in cert_6_text.stdout" that:
- "'DNS:example.org' in cert_6_text.stdout"
when: acme_intermediates[0].subject_key_identifier is defined
- block:
- name: Check that certificate 7 is valid
assert:
that:
- cert_7_valid is not failed
- name: Check that certificate 7 contains correct SANs
assert:
that:
- "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout"
when: acme_roots[2].subject_key_identifier is defined
- block:
- name: Check that certificate 8 is valid
assert:
that:
- cert_8_valid is not failed
- name: Check that certificate 8 contains correct SANs
assert:
that:
- "'IP Address:127.0.0.1' in cert_8_text.stdout or 'IP:127.0.0.1' in cert_8_text.stdout"
when: cryptography_version.stdout is version('1.3', '>=')
- name: Validate that orders were not retrieved - name: Validate that orders were not retrieved
assert: assert:

View File

@ -31,4 +31,4 @@
terms_agreed: yes terms_agreed: yes
account_email: "example@example.org" account_email: "example@example.org"
when: openssl_version.stdout is version('1.0.0', '>=') or cryptography_version.stdout is version('1.5', '>=') when: cryptography_version.stdout is version('1.5', '>=')

View File

@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_acme - setup_acme
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -4,16 +4,35 @@
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
- set_fact:
skip_tests: false
- block: - block:
- name: Get servers certificate with backend auto-detection - name: Get servers certificate with backend auto-detection
get_certificate: get_certificate:
host: "{{ httpbin_host }}" host: "{{ httpbin_host }}"
port: 443 port: 443
ignore_errors: true
register: result
- set_fact:
skip_tests: |
{{
result is failed and (
'error: [Errno 1] _ssl.c:492: error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure' in result.msg
or
'error: _ssl.c:314: Invalid SSL protocol variant specified.' in result.msg
)
}}
- assert:
that:
- result is success or skip_tests
when: | when: |
pyopenssl_version.stdout is version('0.15', '>=') or pyopenssl_version.stdout is version('0.15', '>=') or
(cryptography_version.stdout is version('1.6', '>=') and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)) cryptography_version.stdout is version('1.6', '>=')
- block: - block:
@ -21,7 +40,7 @@
vars: vars:
select_crypto_backend: pyopenssl select_crypto_backend: pyopenssl
when: pyopenssl_version.stdout is version('0.15', '>=') when: pyopenssl_version.stdout is version('0.15', '>=') and not skip_tests
- block: - block:
@ -32,6 +51,4 @@
# The module doesn't work with CentOS 6. Since the pyOpenSSL installed there is too old, # The module doesn't work with CentOS 6. Since the pyOpenSSL installed there is too old,
# we never noticed before. This becomes a problem with the new cryptography backend, # we never noticed before. This becomes a problem with the new cryptography backend,
# since there is a new enough cryptography version... # since there is a new enough cryptography version...
when: | when: cryptography_version.stdout is version('1.6', '>=') and not skip_tests
cryptography_version.stdout is version('1.6', '>=') and
(ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)

View File

@ -2,3 +2,4 @@ dependencies:
- setup_openssl - setup_openssl
- setup_pyopenssl - setup_pyopenssl
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -2,3 +2,4 @@ dependencies:
- setup_openssl - setup_openssl
- setup_pyopenssl - setup_pyopenssl
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -2,3 +2,4 @@ dependencies:
- setup_openssl - setup_openssl
- setup_pyopenssl - setup_pyopenssl
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -0,0 +1,135 @@
# Copyright 2007 Pallets
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from jinja2.filters import contextfilter
from jinja2.runtime import Undefined
from jinja2.exceptions import TemplateRuntimeError, FilterArgumentError
try:
from jinja2.nodes import EvalContext
HAS_EVALCONTEXT = True
except ImportError:
HAS_EVALCONTEXT = False
def call_test(environment, test_name, value, args, kwargs):
try:
return environment.call_test(test_name, value, args, kwargs)
except AttributeError:
# call_test was added together with selectattr...
func = environment.tests.get(test_name)
if func is None:
raise TemplateRuntimeError('no test named %r' % test_name)
return func(value, *args, **kwargs)
def call_filter(environment, name, value, args=None, kwargs=None,
context=None, eval_ctx=None):
func = environment.filters.get(name)
if func is None:
raise TemplateRuntimeError('no filter named %r' % name)
args = list(args or ())
if getattr(func, 'contextfilter', False):
if context is None:
raise TemplateRuntimeError('Attempted to invoke context filter without context')
args.insert(0, context)
elif getattr(func, 'evalcontextfilter', False):
if eval_ctx is None:
if context is not None:
eval_ctx = context.eval_ctx
elif HAS_EVALCONTEXT:
eval_ctx = EvalContext(environment)
else:
raise TemplateRuntimeError('Too old Jinja2 does not have EvalContext')
args.insert(0, eval_ctx)
elif getattr(func, 'environmentfilter', False):
args.insert(0, environment)
return func(value, *args, **(kwargs or {}))
def make_attrgetter(environment, attribute_str, default=None):
attributes = [int(attribute) if attribute.isdigit() else attribute for attribute in attribute_str.split(".")]
def f(item):
for attribute in attributes:
item = environment.getitem(item, attribute)
if default and isinstance(item, Undefined):
item = default
return item
return f
@contextfilter
def compatibility_selectattr_filter(context, sequence, attribute_str, test_name, *args, **kwargs):
f = make_attrgetter(context.environment, attribute_str)
for item in sequence:
if call_test(context.environment, test_name, f(item), args, kwargs):
yield item
def prepare_map(context, args, kwargs):
if len(args) == 0 and "attribute" in kwargs:
attribute = kwargs.pop("attribute")
default = kwargs.pop("default", None)
if kwargs:
raise FilterArgumentError("Unexpected keyword argument {0!r}".format(next(iter(kwargs))))
func = make_attrgetter(context.environment, attribute, default=default)
else:
try:
name = args[0]
args = args[1:]
except LookupError:
raise FilterArgumentError("map requires a filter argument")
def func(item):
return call_filter(context.environment, name, item, args, kwargs, context=context)
return func
@contextfilter
def compatibility_map_filter(context, seq, *args, **kwargs):
func = prepare_map(context, args, kwargs)
if seq:
for item in seq:
yield func(item)
class FilterModule:
''' Jinja2 compat filters '''
def filters(self):
return {
'selectattr': compatibility_selectattr_filter,
'map': compatibility_map_filter,
}

View File

@ -0,0 +1 @@
---

View File

@ -2,6 +2,10 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
def compatibility_equalto_test(a, b):
return a == b
def compatibility_in_test(a, b): def compatibility_in_test(a, b):
return a in b return a in b
@ -11,5 +15,6 @@ class TestModule:
def tests(self): def tests(self):
return { return {
'equalto': compatibility_equalto_test,
'in': compatibility_in_test, 'in': compatibility_in_test,
} }

View File

@ -11,7 +11,7 @@
'secp384r1' if key_type == 'ec384' else 'secp384r1' if key_type == 'ec384' else
'secp521r1' if key_type == 'ec521' else 'secp521r1' if key_type == 'ec521' else
'invalid value for key_type!' }} 'invalid value for key_type!' }}
passphrase: "{{ certificate_passphrase | default(omit, true) }}" passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}" cipher: "{{ 'auto' if certificate_passphrase | default() else omit }}"
force: true force: true
## CSR ######################################################################################## ## CSR ########################################################################################
@ -19,7 +19,7 @@
openssl_csr: openssl_csr:
path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr" path: "{{ remote_tmp_dir }}/{{ certificate_name }}.csr"
privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" privatekey_path: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
privatekey_passphrase: "{{ certificate_passphrase | default(omit, true) }}" privatekey_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
subject_alt_name: "{{ subject_alt_name }}" subject_alt_name: "{{ subject_alt_name }}"
subject_alt_name_critical: "{{ subject_alt_name_critical }}" subject_alt_name_critical: "{{ subject_alt_name_critical }}"
return_content: true return_content: true
@ -33,7 +33,7 @@
validate_certs: no validate_certs: no
account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}"
account_key_content: "{{ account_key_content | default(omit) }}" account_key_content: "{{ account_key_content | default(omit) }}"
account_key_passphrase: "{{ account_key_passphrase | default(omit, true) }}" account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}"
modify_account: "{{ modify_account }}" modify_account: "{{ modify_account }}"
csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}"
csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}" csr_content: "{{ csr_result.csr if use_csr_content | default(false) else omit }}"
@ -68,21 +68,21 @@
body: "{{ item.value }}" body: "{{ item.value }}"
with_dict: "{{ challenge_data.challenge_data_dns }}" with_dict: "{{ challenge_data.challenge_data_dns }}"
when: "challenge_data is changed and challenge == 'dns-01'" when: "challenge_data is changed and challenge == 'dns-01'"
- name: ({{ certgen_title }}) Create TLS ALPN challenges (acm_challenge_cert_helper) - name: ({{ certgen_title }}) Create TLS ALPN challenges (acme_challenge_cert_helper)
acme_challenge_cert_helper: acme_challenge_cert_helper:
challenge: tls-alpn-01 challenge: tls-alpn-01
challenge_data: "{{ item.value['tls-alpn-01'] }}" challenge_data: "{{ item.value['tls-alpn-01'] }}"
private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key" private_key_src: "{{ remote_tmp_dir }}/{{ certificate_name }}.key"
private_key_passphrase: "{{ certificate_passphrase | default(omit, true) }}" private_key_passphrase: "{{ certificate_passphrase | default(omit) | default(omit, true) }}"
with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper') else {} }}" with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else {} }}"
register: tls_alpn_challenges register: tls_alpn_challenges
when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper')" when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
- name: ({{ certgen_title }}) Read private key - name: ({{ certgen_title }}) Read private key
slurp: slurp:
src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key' src: '{{ remote_tmp_dir }}/{{ certificate_name }}.key'
register: slurp register: slurp
when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper')" when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
- name: ({{ certgen_title }}) Set TLS ALPN challenges (acm_challenge_cert_helper) - name: ({{ certgen_title }}) Set TLS ALPN challenges (acme_challenge_cert_helper)
uri: uri:
url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key" url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.domain }}/{{ item.identifier }}/certificate-and-key"
method: PUT method: PUT
@ -90,8 +90,8 @@
body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}" body: "{{ item.challenge_certificate }}\n{{ slurp.content | b64decode }}"
headers: headers:
content-type: "application/pem-certificate-chain" content-type: "application/pem-certificate-chain"
with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper') else [] }}" with_items: "{{ tls_alpn_challenges.results if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper') else [] }}"
when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is defined and challenge_alpn_tls == 'acme_challenge_cert_helper')" when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'acme_challenge_cert_helper')"
- name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64) - name: ({{ certgen_title }}) Create TLS ALPN challenges (der-value-b64)
uri: uri:
url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64" url: "http://{{ acme_host }}:5000/tls-alpn/{{ item.value['tls-alpn-01'].resource }}/{{ item.value['tls-alpn-01'].resource_original }}/der-value-b64"
@ -100,8 +100,8 @@
body: "{{ item.value['tls-alpn-01'].resource_value }}" body: "{{ item.value['tls-alpn-01'].resource_value }}"
headers: headers:
content-type: "application/octet-stream" content-type: "application/octet-stream"
with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is not defined or challenge_alpn_tls == 'der-value-b64') else [] }}" with_dict: "{{ challenge_data.challenge_data if challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64') else {} }}"
when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls is not defined or challenge_alpn_tls == 'der-value-b64')" when: "challenge_data is changed and challenge == 'tls-alpn-01' and (challenge_alpn_tls | default('der-value-b64') == 'der-value-b64')"
## ACME STEP 2 ################################################################################ ## ACME STEP 2 ################################################################################
- name: ({{ certgen_title }}) Obtain cert, step 2 - name: ({{ certgen_title }}) Obtain cert, step 2
acme_certificate: acme_certificate:
@ -111,7 +111,7 @@
validate_certs: no validate_certs: no
account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}" account_key: "{{ (remote_tmp_dir ~ '/' ~ account_key ~ '.pem') if account_key_content is not defined else omit }}"
account_key_content: "{{ account_key_content | default(omit) }}" account_key_content: "{{ account_key_content | default(omit) }}"
account_key_passphrase: "{{ account_key_passphrase | default(omit, true) }}" account_key_passphrase: "{{ account_key_passphrase | default(omit) | default(omit, true) }}"
account_uri: "{{ challenge_data.account_uri }}" account_uri: "{{ challenge_data.account_uri }}"
modify_account: "{{ modify_account }}" modify_account: "{{ modify_account }}"
csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}" csr: "{{ omit if use_csr_content | default(false) else remote_tmp_dir ~ '/' ~ certificate_name ~ '.csr' }}"

View File

@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_python_info
- setup_remote_constraints - setup_remote_constraints
- setup_pkg_mgr - setup_pkg_mgr

View File

@ -8,7 +8,9 @@
command: "{{ ansible_python.executable }} -c 'import os; print(dict(os.environ))'" command: "{{ ansible_python.executable }} -c 'import os; print(dict(os.environ))'"
register: sys_environment register: sys_environment
- debug: var=sys_environment - name: Show system environment
debug:
var: sys_environment.stdout_lines
- name: Default value for OpenSSL binary path - name: Default value for OpenSSL binary path
set_fact: set_fact:
@ -18,14 +20,19 @@
include_vars: '{{ ansible_os_family }}.yml' include_vars: '{{ ansible_os_family }}.yml'
when: not ansible_os_family == "Darwin" when: not ansible_os_family == "Darwin"
- name: Check whether OpenSSL is there
command: "{{ openssl_binary }} version"
register: openssl_version_full
ignore_errors: true
- name: Install OpenSSL - name: Install OpenSSL
become: true become: true
package: package:
name: '{{ openssl_package_name }}' name: '{{ openssl_package_name }}'
when: not ansible_os_family == 'Darwin' when: not ansible_os_family == 'Darwin' and openssl_version_full is failed
- name: Register openssl version (full) - name: Register openssl version (full)
shell: "{{ openssl_binary }} version" command: "{{ openssl_binary }} version"
register: openssl_version_full register: openssl_version_full
- name: Show openssl version (full) - name: Show openssl version (full)
@ -60,7 +67,7 @@
openssl_binary: "{{ brew_openssl_prefix.stdout }}/bin/openssl" openssl_binary: "{{ brew_openssl_prefix.stdout }}/bin/openssl"
- name: MACOS | Register openssl version (full) - name: MACOS | Register openssl version (full)
shell: "{{ openssl_binary }} version" command: "{{ openssl_binary }} version"
register: openssl_version_full_again register: openssl_version_full_again
# We must use a different variable to prevent the 'when' condition of the surrounding block to fail # We must use a different variable to prevent the 'when' condition of the surrounding block to fail
@ -69,29 +76,37 @@
var: openssl_version_full_again.stdout_lines var: openssl_version_full_again.stdout_lines
- name: Register openssl version - name: Register openssl version
shell: "{{ openssl_binary }} version | cut -d' ' -f2" shell: "{{ openssl_binary }} version | cut -d' ' -f2"
register: openssl_version register: openssl_version
- when: ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['CentOS6', 'RedHat6'] - when: ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['CentOS6', 'RedHat6']
block: block:
- name: Install cryptography (Python 3)
become: true
package:
name: '{{ cryptography_package_name_python3 }}'
when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '>=')
- name: Install cryptography (Python 2) - name: Install from system packages
become: true when: ansible_os_family != "Darwin" and target_system_python
package: block:
name: '{{ cryptography_package_name }}'
when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '<')
- name: Install cryptography (Darwin) - name: Install cryptography (Python 3 from system packages)
become: true become: true
pip: package:
name: cryptography>=3.3 name: '{{ cryptography_package_name_python3 }}'
extra_args: "-c {{ remote_constraints }}" when: ansible_python_version is version('3.0', '>=')
when: ansible_os_family == 'Darwin'
- name: Install cryptography (Python 2 from system packages)
become: true
package:
name: '{{ cryptography_package_name }}'
when: ansible_python_version is version('3.0', '<')
- name: Install from PyPi
when: ansible_os_family == "Darwin" or not target_system_python
block:
- name: Install cryptography (PyPi)
become: true
pip:
name: 'cryptography{% if ansible_os_family == "Darwin" %}>=3.3{% endif %}'
extra_args: "-c {{ remote_constraints }}"
- name: Register cryptography version - name: Register cryptography version
command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'"

View File

@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_python_info
- setup_remote_constraints - setup_remote_constraints
- setup_pkg_mgr - setup_pkg_mgr

View File

@ -4,28 +4,34 @@
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
- name: Include OS-specific variables - name: Install from system packages
include_vars: '{{ ansible_os_family }}.yml' when: ansible_os_family != "Darwin" and target_system_python
when: not ansible_os_family == "Darwin" block:
- name: Install pyOpenSSL (Python 3) - name: Include OS-specific variables
become: true include_vars: '{{ ansible_os_family }}.yml'
package:
name: '{{ pyopenssl_package_name_python3 }}'
when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '>=')
- name: Install pyOpenSSL (Python 2) - name: Install pyOpenSSL (Python 3 from system packages)
become: true become: true
package: package:
name: '{{ pyopenssl_package_name }}' name: '{{ pyopenssl_package_name_python3 }}'
when: not ansible_os_family == 'Darwin' and ansible_python_version is version('3.0', '<') when: ansible_python_version is version('3.0', '>=')
- name: Install pyOpenSSL (Darwin) - name: Install pyOpenSSL (Python 2 from system packages)
become: true become: true
pip: package:
name: pyOpenSSL name: '{{ pyopenssl_package_name }}'
extra_args: "-c {{ remote_constraints }}" when: ansible_python_version is version('3.0', '<')
when: ansible_os_family == 'Darwin'
- name: Install from PyPi
when: ansible_os_family == "Darwin" or not target_system_python
block:
- name: Install pyOpenSSL (PyPi)
become: true
pip:
name: pyOpenSSL
extra_args: "-c {{ remote_constraints }}"
- name: Register pyOpenSSL version - name: Register pyOpenSSL version
command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'" command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'"

View File

@ -0,0 +1,33 @@
# (c) 2021, Felix Fontein <felix@fontein.de>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
def get_major_minor_version(version):
parts = version.split('.')[:2]
return '.'.join(parts)
class FilterModule(object):
""" IP address and network manipulation filters """
def filters(self):
return {
'internal__get_major_minor_version': get_major_minor_version,
}

View File

@ -0,0 +1,55 @@
---
- name: Gather facts on controller
setup:
gather_subset: '!all'
delegate_to: localhost
delegate_facts: true
run_once: true
- name: Show variables
debug:
msg: |-
Target:
Python: {{ ansible_facts.python.version.major ~ '.' ~ ansible_facts.python.version.minor }}
OS family: {{ ansible_facts.os_family }}
Distribution: {{ ansible_facts.distribution }}
Distribution version: {{ ansible_facts.distribution_version | internal__get_major_minor_version }}
Distribution major version: {{ ansible_facts.distribution_major_version }}
Controller:
Python: {{ hostvars['localhost'].ansible_facts.python.version.major ~ '.' ~ hostvars['localhost'].ansible_facts.python.version.minor }}
OS family: {{ hostvars['localhost'].ansible_facts.os_family }}
Distribution: {{ hostvars['localhost'].ansible_facts.distribution }}
Distribution version: {{ hostvars['localhost'].ansible_facts.distribution_version | internal__get_major_minor_version }}
Distribution major version: {{ hostvars['localhost'].ansible_facts.distribution_major_version }}
- name: Record information
set_fact:
target_system_python: >-
{{
(ansible_facts.python.version.major ~ '.' ~ ansible_facts.python.version.minor)
in
(
system_python_version_data[ansible_facts.distribution] |
default(system_python_version_data[ansible_facts.os_family])
)[ansible_facts.distribution_version | internal__get_major_minor_version]
| default(
(
system_python_version_data[ansible_facts.distribution] |
default(system_python_version_data[ansible_facts.os_family])
)[ansible_facts.distribution_major_version]
)
}}
controller_system_python: >-
{{
(hostvars['localhost'].ansible_facts.python.version.major ~ '.' ~ hostvars['localhost'].ansible_facts.python.version.minor)
in
(
system_python_version_data[hostvars['localhost'].ansible_facts.distribution] |
default(system_python_version_data[hostvars['localhost'].ansible_facts.os_family])
)[ansible_facts.distribution_version | internal__get_major_minor_version]
| default(
(
system_python_version_data[hostvars['localhost'].ansible_facts.distribution] |
default(system_python_version_data[hostvars['localhost'].ansible_facts.os_family])
)[hostvars['localhost'].ansible_facts.distribution_major_version]
)
}}

View File

@ -0,0 +1,50 @@
---
system_python_version_data:
CentOS:
'6':
- '2.6'
'7':
- '2.7'
'8':
- '3.6'
Fedora:
'30':
- '3.7'
'31':
- '3.7'
'32':
- '3.8'
'33':
- '3.9'
'34':
- '3.9'
Ubuntu:
'16':
- '2.7'
'18':
- '3.6'
'20':
- '3.8'
Darwin:
'10.11':
- '2.7'
'10.15':
- '3.8'
'11.1':
- '3.9'
FreeBSD:
'12.1':
- '3.6'
'12.2':
- '3.7'
'13.0':
- '3.7'
RedHat:
'7':
- '2.7'
'8':
- '3.6'
Suse:
'15':
- '2.7'
- '3.6'

View File

@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_acme - setup_acme
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -109,6 +109,13 @@
regexp: 'os\.remove\(wellknown_path\)' regexp: 'os\.remove\(wellknown_path\)'
replace: 'pass' replace: 'pass'
- name: "Monkey-patch acme-tiny: Allow to run with Python 2"
replace:
path: "{{ remote_tmp_dir }}/acme-tiny"
regexp: '#!/usr/bin/env python3'
replace: '#!/usr/bin/env python'
when: ansible_facts.python.version.major == 2
- name: Create challenges directory - name: Create challenges directory
file: file:
path: '{{ remote_tmp_dir }}/challenges' path: '{{ remote_tmp_dir }}/challenges'

View File

@ -2,3 +2,4 @@ dependencies:
- setup_openssl - setup_openssl
- setup_pyopenssl - setup_pyopenssl
- setup_remote_tmp_dir - setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@ -1,15 +0,0 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
def compatibility_in_test(a, b):
return a in b
class TestModule:
''' Ansible math jinja2 tests '''
def tests(self):
return {
'in': compatibility_in_test,
}