Build Windows Templates in RHV

This commit is contained in:
2021-05-03 13:47:44 -04:00
parent 595021d449
commit 28c9375b0d
290 changed files with 10931 additions and 159 deletions

View File

@@ -0,0 +1,12 @@
- name: create an ovirt windows template
hosts: windows_template_base
gather_facts: False
connection: local
become: no
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
roles:
- oatakan.windows_ovirt_template

7
buildvm_artifact.json Normal file
View File

@@ -0,0 +1,7 @@
{
"version": "1.0.0",
"plays": [],
"stdout": [],
"status": "successful",
"status_color": 10
}

View File

@@ -1,76 +0,0 @@
ack
asciidoctor
asciinema
aspell-dict-ca
aspell-dict-en
aspell-dict-uk
astyle
automake
avahi
cdrtools
certbot
cracklib
curl
dash
davix
db62
dbus-python37
dupd
emacs
emacs-mac-app-devel
fd
ffmpeg
gconf
gdk-pixbuf2
git-delta
gmime
gnutar
go
gtk2
gtk3
html2text
icedtea6-plugs
ipmitool
isync
jq
kubectl-1.17
libglade2
libvterm
minicom
mpvim
msmtp
mtr
mu
ncdu
nut
nvm
offlineimap
oniguruma6
openconnect
OpenIPMI
openjfx11
openssh
p5.28-yaml-libyaml
pass
perl5
php-crack
plantuml
Platypus
poppler
py-boto3
py-libxml2
py27-opengl-accelerate
py27-pygtk
py37-curl
py37-msgpack
py37-SDL2
py38-powerline
py38-virtualenvwrapper
ranger
ripgrep
sassc
terminal-notifier
topgrade
virt-viewer
xapian-bindings-python27
yarn

View File

@@ -73,13 +73,27 @@
src: "{{ key_files_prefix}}-rootchain.pem"
dest: /etc/pki/ca-trust/source/anchors/
register: rootchain_result
notify: restart httpd
notify:
- update ca-trust
- restart httpd
- name: Certificate store updated
command: /usr/bin/update-ca-trust
when: rootchain_result.changed
notify: restart httpd
- name: Apache CA is file, not link
file:
path: /etc/pki/ovirt-engine/apache-ca.pem
state: file
register: apache_ca_stat
- name: Apache CA link is removed
file:
path: /etc/pki/ovirt-engine/apache-ca.pem
state: absent
when: apache_ca_stat.state == "file"
- name: CA Rootchain in Apache config
copy:
src: "{{ key_files_prefix }}-rootchain.pem"
@@ -92,6 +106,9 @@
src: "{{ key_files_prefix }}.key"
dest: /etc/pki/ovirt-engine/keys/apache.key.nopass
backup: yes
owner: root
group: ovirt
mode: 640
notify: restart httpd
- name: Certificate installed
@@ -99,6 +116,9 @@
src: "{{ key_files_prefix }}.pem"
dest: /etc/pki/ovirt-engine/certs/apache.cer
backup: yes
owner: root
group: ovirt
mode: 644
notify: restart httpd
- name: Trust Store Configuration
@@ -121,8 +141,7 @@
- SSL_CERTIFICATE=/etc/pki/ovirt-engine/apache.cer
- SSL_KEY=/etc/pki/ovirt-engine/keys/apache.key.nopass
notify:
- restart ovn
- restart ovirt-engine
- restart ovirt-websocket-proxy
handlers:
- name: restart httpd
@@ -130,6 +149,9 @@
name: httpd
state: restarted
- name: update ca-trust
command: update-ca-trust
- name: restart ovn
service:
name: ovirt-provider-ovn
@@ -140,6 +162,11 @@
name: ovirt-engine
state: restarted
- name: restart ovirt-websocket-proxy
service:
name: ovirt-websocket-proxy
state: restarted
- name: Create RHV/ovirt VLANs
hosts: rhv.mgmt.toal.ca

View File

@@ -0,0 +1 @@
__pycache__

View File

@@ -0,0 +1,45 @@
---
extends: default
rules:
line-length:
max: 140
level: warning
document-start:
present: true
document-end:
present: false
truthy:
level: error
allowed-values:
- 'yes'
- 'no'
- 'true'
- 'false'
- 'True'
- 'False'
indentation:
spaces: 2
indent-sequences: consistent
key-duplicates: enable
trailing-spaces: enable
new-line-at-end-of-file: disable
hyphens:
max-spaces-after: 1
empty-lines:
max: 2
max-start: 0
max-end: 0
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
colons:
max-spaces-before: 0
max-spaces-after: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
braces:
min-spaces-inside: 0
max-spaces-inside: 0

View File

@@ -0,0 +1,19 @@
# Changelog for acme_certificate
## Version 1.1.1 (2020-05-22)
- Linting, to make Galaxy more happy. (ansible-lint does not like missing modules. This might get better with collections.)
## Version 1.1.0 (2020-05-22)
- Added better namespacing for role parameters; all role parameters now start with `acme_certificate_`. The old, shorter names can still be used for now. Support for them will be dropped in version 2.0.0, to be released later this year.
- Dropped support for GCDNS (which never worked).
- Support for DNS provider NS1 for DNS challenges (thanks to @timelapserduck).
- Lint YAML files (thanks to @pgporada).
- Allow `key_path` to not have trailing slash (thanks to @nwmcsween).
- Fix curve used for P-256.
- Require Ansible 2.8.3.
## Version 1.0 (2019-07-01)
First version published on Ansible Galaxy.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015--2020 Felix Fontein
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,360 @@
# acme_certificate 1.1.1
Allows to obtain certificates from Let's Encrypt with minimal interaction with the webserver. Most code is executed on the controller, and the account key is never send to the nodes.
The role can be installed via [Ansible Galaxy](https://galaxy.ansible.com/felixfontein/acme_certificate):
ansible-galaxy install felixfontein.acme_certificate
For changes in this role, see [the changelog](CHANGELOG.md).
## Description
This is an [Ansible](https://github.com/ansible/ansible) role which can use any CA supporting the ACME protocol, such as [Let's Encrypt](https://letsencrypt.org/) or [Buypass](https://www.buypass.com/ssl/products/acme), to issue TLS/SSL certificates for your server. This role requires Ansible 2.8.3 or newer and is based on the [acme_certificate module](https://docs.ansible.com/ansible/latest/acme_certificate_module.html) coming with Ansible.
The main advantage of this approach over others is that *almost no code is executed on your webserver*: only when you use HTTP challenges, files need to be copied onto your webserver, and afterwards deleted from it. Everything else is executed on your local machine!
(This does not cover installing the certificates, you have to do that yourself in another role.)
## Requirements
Requires the Python [cryptography](https://pypi.org/project/cryptography/) library installed on the controller, available to the Python version used to execute the playbook. If `cryptography` is not installed, a recent enough version of [PyOpenSSL](https://pypi.org/project/pyOpenSSL/) is currently supported as a fallback by the Ansible `openssl_privatekey` and `openssl_csr` modules.
The `openssl` binary must also be available in the executable path on the controller. It is needed by the `acme_certificate` module in case `cryptography` is not installed, and it is used for certificate chain validation.
If DNS challenges are used, there can be other requirements depending on the DNS provider. For example, for Amazon's Route 53, the Ansible `route53` module requires the Python `boto` package.
## Account Key Setup
You can create an account key using the `openssl` binary as follows:
# RSA 4096 bit key
openssl genrsa 4096 -out keys/acme-account.key
# ECC 256 bit key (P-256)
openssl ecparam -name prime256v1 -genkey -out keys/acme-account.key
# ECC 384 bit key (P-384)
openssl ecparam -name secp384r1 -genkey -out keys/acme-account.key
With Ansible, you can use the `openssl_privatekey` module as follows:
- name: Generate RSA 4096 key
openssl_privatekey:
path: keys/acme-account.key
type: RSA
size: 4096
- name: Generate ECC 256 bit key (P-256)
openssl_privatekey:
path: keys/acme-account.key
type: ECC
curve: secp256r1
- name: Generate ECC 384 bit key (P-384)
openssl_privatekey:
path: keys/acme-account.key
type: ECC
curve: secp384r1
Make sure you store the account key safely. As opposed to certificate private keys, there is no need to regenerate it frequently, and it makes recovation of certificates issued with it very simple.
## Role Variables
Please note that from May 2020 on, all variables must be prefixed with `acme_certificate_`. For some time, the module will still use the old (short) variable names if the longer ones are not defined. Please upgrade your role usage as soon as possible.
These are the main variables:
- `acme_certificate_acme_account`: Path to the private ACME account key. Must always be specified.
- `acme_certificate_acme_email`: Your email address which shall be associated to the ACME account. Must always be specified.
- `acme_certificate_algorithm`: The algorithm used for creating private keys. The default is `"rsa"`; other choices are `"p-256"`, `"p-384"` or `"p-521"` for the NIST elliptic curves `prime256v1`, `secp384r1` and `secp521r1`, respectively.
- `acme_certificate_key_length`: The bitlength to use for RSA private keys. The default is 4096.
- `acme_certificate_key_name`: The basename for storing the keys and certificates. The default is the first domain specified, with `*` replaced by `_`.
- `acme_certificate_keys_path`: Where the keys and certificates are stored. Default value is `"keys/"`.
- `acme_certificate_keys_old_path`: Where old keys and certificates should be copied to; used in case `acme_certificate_keys_old_store` is true. Default value is `"keys/old/"`.
- `acme_certificate_keys_old_store`: If set to `true`, will make copies of old keys and certificates. The copies will be stored in the directory specified by `acme_certificate_keys_old_store`. Default value is `false`.
- `acme_certificate_keys_old_prepend_timestamp`: Whether copies of old keys and certificates should be prepended by the current date and time. Default value is `false`.
- `acme_certificate_ocsp_must_staple`: Whether a certificate with the OCSP Must Staple extension is requested. Default value is `false`.
- `acme_certificate_agreement`: The terms of service document the user agrees to. Default value is `https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf`.
- `acme_certificate_acme_directory`: The ACME directory to use. Default is `https://acme-v02.api.letsencrypt.org/directory`, which is the current production ACME v2 endpoint of Let's Encrypt.
- `acme_certificate_acme_version`: The ACME directory's version. Default is 2. Use 1 for ACME v1.
- `acme_certificate_challenge`: The challenge type to use. Should be `http-01` for HTTP challenges (needs access to web server) or `dns-01` for DNS challenges (needs access to DNS provider).
- `acme_certificate_root_certificate`: The root certificate for the ACME directory. Default value is `https://letsencrypt.org/certs/isrgrootx1.pem` for the root certificate of Let's Encrypt.
- `acme_certificate_deactivate_authzs`: Whether `authz`s (authorizations) should be deactivated afterwards. Default value is `true`. Set to `false` to be able to re-use `authz`s.
- `acme_certificate_modify_account`: Whether the ACME account should be created (if it doesn't exist) and the contact data (email address) should be updated. Default value is `true`. Set to `false` if you want to use the `acme_account` module to manage your ACME account (not done by this role).
- `acme_certificate_privatekey_mode`: Which file mode to use for the private key file. Default value is `"0600"`, which means read- and writeable by the owner, but not accessible by anyone else (except possibly `root`).
### HTTP Challenges
For HTTP challenges, the following variables define how the challenges can be put onto the (remote) webserver:
- `acme_certificate_server_location`: Location where `.well-known/acme-challenge/` will be served from. Default is `/var/www/challenges`.
- `acme_certificate_http_become`: Argument for `become:` for the `file` and `copy` tasks. Default value is `false`.
- `acme_certificate_http_challenge_user`: The user the challenge files are owned by. Default value is `root`.
- `acme_certificate_http_challenge_group`: The group the challenge files are owned by. Default value is `http`.
- `acme_certificate_http_challenge_folder_mode`: The mode to use for the challenge folder. Default value is `0750` (octal).
- `acme_certificate_http_challenge_file_mode`: The mode to use for the challenge files. Default value is `0640` (octal).
The following subsection shows how to configure [nginx](https://nginx.org/) for HTTP challenges. Configuring other webservers can be done in a similar way.
#### Nginx configuration
Assume that for one of your TLS/SSL protected domains, you use a HTTP-to-HTTPS redirect. Let's assume it looks like this:
server {
listen example.com:80;
server_name example.com *.example.com;
return 301 https://www.example.com$request_uri;
}
To allow the `acme_certificate` role to put something at `http://*.example.com/.well-known/acme-challenge/`, you can change this to:
server {
listen example.com:80;
server_name example.com *.example.com;
location /.well-known/acme-challenge/ {
alias /var/www/challenges/;
try_files $uri =404;
}
location / {
return 301 https://www.example.com$request_uri;
}
}
With this nginx config, all other URLs on `*.example.com` and `example.com` are still redirected, while everything in `*.example.com/.well-known/acme-challenge/` is served from `/var/www/challenges`. When adjusting the location of `/var/www/challenges`, you must also change `acme_certificate_server_location`.
You can even improve on this by redirecting all URLs in `*.example.com/.well-known/acme-challenge/` which do not resolve to a valid file in `/var/www/challenges` to your HTTPS server as well. One way to do this is:
server {
listen example.com:80;
server_name example.com *.example.com;
location /.well-known/acme-challenge/ {
alias /var/www/lechallenges/;
try_files $uri @forward_https;
}
location @forward_https {
return 301 https://www.example.com$request_uri;
}
location / {
return 301 https://www.example.com$request_uri;
}
}
With this config, if `/var/www/challenges/` is empty, your HTTP server will behave as if the `/.well-known/acme-challenge/` location isn't specified.
### DNS Challenges
If DNS challenges are used, the following variables define how the challenges can be fulfilled:
- `acme_certificate_dns_provider`: must be one of `route53`, `hosttech`, and `ns1`. Each needs more information:
- For `route53` (Amazon Route 53), the credentials must be passed as `acme_certificate_aws_access_key` and `acme_certificate_aws_secret_key`.
- For `hosttech` (hosttech GmbH, requires external [hosttech_dns_record module](https://github.com/felixfontein/ansible-hosttech)).
- For `ns1` ([ns1.com](https://ns1.com)) the key for your API account must be passed as `acme_certificate_ns1_secret_key`. Also it depends on external module `ns1_record`. Assuming default directory structure and settings, you may need download 2 files into machine where playbook executed:
```bash
curl --create-dirs -L -o ~/.ansible/plugins/module_utils/ns1.py https://github.com/ns1/ns1-ansible-modules/raw/master/module_utils/ns1.py
curl --create-dirs -L -o ~/.ansible/plugins/modules/ns1_record.py https://github.com/ns1/ns1-ansible-modules/raw/master/library/ns1_record.py
```
Please note that the DNS challenge code is not perfect. The Route 53, Hosttech and NS1 functionality has been tested. One thing that is not complete yet is that the code tries to extract the DNS zone from the domain by taking the last two components separated by dots. This will fail for example for `.co.uk` domains or other nested zones.
Support for more DNS providers can be added by adding `tasks/dns-NAME-create.yml` and `tasks/dns-NAME-cleanup.yml` files with similar content as in the existing files.
## Account key conversion
Note that this Ansible role expects the Let's Encrypt account key to be in PEM format and not in JWK format, which is used by the [official Let's Encrypt client certbot](https://github.com/certbot/certbot). If you have created an account key with the official client and now want to use this key with this ansible role, you have to convert it. One tool which can do this is [pem-jwk](https://github.com/dannycoates/pem-jwk).
## Generated Files
Let's assume you created TLS keys for `www.example.com`. You have to copy the relevant files to your webserver. The ansible role created the following files:
* `keys/www.example.com.key`: this is the private key for the certificate. Ensure nobody can access it.
* `keys/www.example.com.pem`: this is the certificate itself.
* `keys/www.example.com-chain.pem`: this is the intermediate certificate(s) needed for a trust path.
* `keys/www.example.com.cnf`: this is an OpenSSL configuration file used to create the Certificate Signing Request. You can safely delete it.
* `keys/www.example.com.csr`: this is the Certificate Signing Request used to obtain the certificate. You can safely delete it.
* `keys/www.example.com-fullchain.pem`: this is the certificate combined with the intermediate certificate(s).
* `keys/www.example.com-rootchain.pem`: this is the intermediate certificate(s) combined with the root certificate. You might need this for OCSP stapling.
* `keys/www.example.com-root.pem`: this is the root certificate of Let's Encrypt.
For configuring your webserver, you need the private key (`keys/www.example.com.key`), and either the certificate with intermediate certificate(s) combined in one file (`keys/www.example.com-fullchain.pem`), or the certificate and the intermediate certificate(s) as two separate files (`keys/www.example.com.pem` and `keys/www.example.com-chain.pem`). If you want to use [OCSP stapling](https://en.wikipedia.org/wiki/OCSP_stapling), you will also need `keys/www.example.com-rootchain.pem`.
To get these files onto your web server, you could add tasks as follows:
- name: copy private keys
copy:
src: keys/{{ item }}
dest: /etc/ssl/private/
owner: root
group: root
mode: "0400"
with_items:
- www.example.com.key
notify: reload webserver
- name: copy certificates
copy:
src: keys/{{ item }}
dest: /etc/ssl/server-certs/
owner: root
group: root
mode: "0444"
with_items:
- www.example.com-rootchain.pem
- www.example.com-fullchain.pem
- www.example.com.pem
notify: reload webserver
The webserver configuration could look as follows (for nginx):
server {
listen www.example.com:443 ssl; # IPv4: listen to IP www.example.com points to
listen [::]:443 ssl; # IPv6: listen to localhost
server_name www.example.com;
# Allowing only TLS 1.0 and 1.2, with a very selective amount of ciphers.
# According to SSL Lab's SSL server test, this will block:
# - Android 2.3.7
# - IE 6 and 8 under Windows XP
# - Java 6, 7 and 8
# If that's not acceptable for you, choose other cipher lists. Look for
# example at https://wiki.mozilla.org/Security/Server_Side_TLS
ssl_protocols TLSv1.2 TLSv1;
ssl_prefer_server_ciphers on;
ssl_ciphers "-ALL !ADH !aNULL !EXP !EXPORT40 !EXPORT56 !RC4 !3DES !eNULL !NULL !DES !MD5 !LOW ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ECDHE-ECDSA-AES256-SHA ECDHE-RSA-AES256-SHA DHE-RSA-AES256-SHA";
# The certificate chain sent to the browser, as well as the private key.
# Make sure your private key is only accessible by the webserver during
# configuration loading (which by default is done with user root).
ssl_certificate /etc/ssl/server-certs/www.example.com-fullchain.pem;
ssl_certificate_key /etc/ssl/private/www.example.com.key;
# For OCSP stapling, we need a DNS resolver. Here only public Quad9 and
# Google DNS servers are specified; I would prepent them by your hoster's
# DNS servers. You can usually find their IPs in /etc/resolv.conf on your
# webserver.
resolver 9.9.9.9 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 10s;
# Enabling OCSP stapling. Nginx will take care of retrieving the OCSP data
# automatically. See https://wiki.mozilla.org/Security/Server_Side_TLS#OCSP_Stapling
# for details on OCSP stapling.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/server-certs/www.example.com-rootchain.pem;
# Enables a SSL session cache. Adjust the numbers depending on your site's usage.
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 30m;
ssl_session_tickets off;
# You should only use HSTS with proper certificates; the ones from Let's Encrypt
# are fine for this, self-signed ones are not. See MozillaWiki for more details:
# https://wiki.mozilla.org/Security/Server_Side_TLS#HSTS:_HTTP_Strict_Transport_Security
add_header Strict-Transport-Security "max-age=3155760000;";
charset utf-8;
access_log /var/log/nginx/www.example.com.log combined;
error_log /var/log/nginx/www.example.com.log error;
location / {
root /var/www/www.example.com;
index index.html;
}
}
## Dependencies
This role doesn't depend on other roles.
## Example Playbook
This role can be used as follows. Note that it obtains several certificates, and defines variables used for all certificates globally:
---
- name: getting certificates for webserver
hosts: webserver
vars:
acme_certificate_acme_account: 'keys/acme-account.key'
acme_certificate_acme_email: 'mail@example.com'
# For HTTP challenges:
acme_certificate_server_location: '/var/www/challenges/'
acme_certificate_http_challenge_user: root
acme_certificate_http_challenge_group: http
acme_certificate_http_challenge_folder_mode: "0750"
acme_certificate_http_challenge_file_mode: "0640"
# For DNS challenges with route53:
acme_certificate_dns_provider: route53
acme_certificate_aws_access_key: REPLACE_WITH_YOUR_ACCESS_KEY
acme_certificate_aws_secret_key: REPLACE_WITH_YOUR_SECRET_KEY
# For DNS challenges with ns1:
# acme_certificate_dns_provider: ns1
# acme_certificate_ns1_secret_key: REPLACE_WITH_YOUR_SECRET_KEY
roles:
- role: acme_certificate
acme_certificate_domains: ['example.com', 'www.example.com']
# Use DNS challenges:
acme_certificate_challenge: dns-01
# The certificate files will be stored at:
# keys/example.com.key (private key)
# keys/example.com.csr (certificate signing request)
# keys/example.com.pem (certificate)
# keys/example.com.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/example.com-chain.pem (intermediate certificate)
# keys/example.com-fullchain.pem (certificate with intermediate certificate)
# keys/example.com-root.pem (root certificate)
# keys/example.com-rootchain.pem (intermediate certificate with root certificate)
- role: acme_certificate
acme_certificate_domains: ['another.example.com']
acme_certificate_key_name: 'another.example.com-rsa'
acme_certificate_key_length: 4096
# Use HTTP challenges:
acme_certificate_challenge: http-01
# The certificate files will be stored at:
# keys/another.example.com-rsa.key (private key)
# keys/another.example.com-rsa.csr (certificate signing request)
# keys/another.example.com-rsa.pem (certificate)
# keys/another.example.com-rsa.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/another.example.com-rsa-chain.pem (intermediate certificate)
# keys/another.example.com-rsa-fullchain.pem (certificate with intermediate certificate)
# keys/another.example.com-rsa-root.pem (root certificate)
# keys/another.example.com-rsa-rootchain.pem (intermediate certificate with root certificate)
- role: acme_certificate
acme_certificate_domains: ['another.example.com']
acme_certificate_key_name: 'another.example.com-ecc'
acme_certificate_algorithm: 'p-256'
# Use HTTP challenges (default for challenge is http-01).
# The certificate files will be stored at:
# keys/another.example.com-ecc.key (private key)
# keys/another.example.com-ecc.csr (certificate signing request)
# keys/another.example.com-ecc.pem (certificate)
# keys/another.example.com-ecc.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/another.example.com-ecc-chain.pem (intermediate certificate)
# keys/another.example.com-ecc-fullchain.pem (certificate with intermediate certificate)
# keys/another.example.com-ecc-root.pem (root certificate)
# keys/another.example.com-ecc-rootchain.pem (intermediate certificate with root certificate)
## License
The MIT License (MIT)
Copyright (c) 2018-2020 Felix Fontein
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Author Information
The homepage for this role is https://github.com/felixfontein/acme-certificate/. Please use the issue tracker to report problems.

View File

@@ -0,0 +1,2 @@
[defaults]
roles_path = ../

View File

@@ -0,0 +1,47 @@
---
acme_certificate_domains: '{{ domains }}'
acme_certificate_dns_provider: '{{ dns_provider }}'
acme_certificate_acme_account: '{{ acme_account }}'
acme_certificate_acme_email: '{{ acme_email }}'
acme_certificate_algorithm: '{{ algorithm | default("rsa") }}'
acme_certificate_key_length: '{{ key_length | default(4096) }}'
acme_certificate_key_name: "{{ key_name | default(acme_certificate_domains[0].replace('*', '_')) }}"
acme_certificate_keys_path: '{{ keys_path | default("keys/") }}'
acme_certificate_keys_old_path: '{{ keys_old_path | default("keys/old/") }}'
acme_certificate_keys_old_store: '{{ keys_old_store | default(false) }}'
acme_certificate_keys_old_prepend_timestamp: '{{ keys_old_prepend_timestamp | default(false) }}'
acme_certificate_ocsp_must_staple: '{{ ocsp_must_staple | default(false) }}'
acme_certificate_terms_agreed: '{{ terms_agreed | default(true) }}'
acme_certificate_acme_directory: '{{ acme_directory | default("https://acme-v02.api.letsencrypt.org/directory") }}'
acme_certificate_acme_version: '{{ acme_version | default(2) }}'
# For ACME v1:
# acme_certificate_acme_directory: https://acme-v01.api.letsencrypt.org/directory
# acme_certificate_acme_version: 1
# For staging, use:
# acme_certificate_acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory (ACME v2)
# acme_certificate_acme_directory: https://acme-staging.api.letsencrypt.org/directory (ACME v1)
acme_certificate_challenge: '{{ challenge | default("http-01") }}'
acme_certificate_root_certificate: '{{ root_certificate | default("https://letsencrypt.org/certs/isrgrootx1.pem") }}'
# For staging, use:
# root_certificate: https://letsencrypt.org/certs/fakelerootx1.pem
acme_certificate_deactivate_authzs: '{{ deactivate_authzs | default(true) }}'
acme_certificate_modify_account: '{{ modify_account | default(true) }}'
acme_certificate_validate_certs: '{{ validate_certs | default(true) }}'
acme_certificate_verify_certs: '{{ verify_certs | default(true) }}'
acme_certificate_privatekey_mode: '{{ privatekey_mode | default("0600") }}'
# For HTTP challenges:
acme_certificate_server_location: '{{ server_location | default("/var/www/challenges") }}'
acme_certificate_http_become: '{{ http_become | default(false) }}'
acme_certificate_http_challenge_user: '{{ http_challenge_user | default("root") }}'
acme_certificate_http_challenge_group: '{{ http_challenge_group | default("http") }}'
acme_certificate_http_challenge_folder_mode: '{{ http_challenge_folder_mode | default("0750") }}'
acme_certificate_http_challenge_file_mode: '{{ http_challenge_file_mode | default("0640") }}'
# DNS challenge credentials
acme_certificate_hosttech_username: '{{ hosttech_username | default(omit) }}'
acme_certificate_hosttech_password: '{{ hosttech_password | default(omit) }}'
acme_certificate_ns1_secret_key: '{{ ns1_secret_key | default(omit) }}'
acme_certificate_aws_access_key: '{{ aws_access_key | default(omit) }}'
acme_certificate_aws_secret_key: '{{ aws_secret_key | default(omit) }}'

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os.path
def path_join(list):
return os.path.join(*list)
class FilterModule(object):
''' Ansible core jinja2 filters '''
def filters(self):
return {
'path_join': path_join,
}

View File

@@ -0,0 +1,2 @@
install_date: Mon Apr 19 15:24:27 2021
version: 1.1.1

View File

@@ -0,0 +1,25 @@
---
galaxy_info:
role_name: acme_certificate
author: Felix Fontein
description: >
Wrapper of Ansible's included acme_certificate module, whose aim is that almost no code
is executed on the webserver. Requires the Python cryptography library as well as the
OpenSSL binary installed locally and available on executable path.
license: MIT
min_ansible_version: 2.8.3
galaxy_tags:
- acme
- letsencrypt
- buypass
- ssl
- tls
- https
- encryption
- security
- web
dependencies: []

View File

@@ -0,0 +1,62 @@
---
- name: getting certificates for webserver
hosts: webserver
vars:
acme_certificate_acme_account: 'keys/acme-account.key'
acme_certificate_acme_email: 'mail@example.com'
# For HTTP challenges:
acme_certificate_server_location: '/var/www/challenges/'
acme_certificate_http_challenge_user: root
acme_certificate_http_challenge_group: http
acme_certificate_http_challenge_folder_mode: "0750"
acme_certificate_http_challenge_file_mode: "0640"
# For DNS challenges with route53:
acme_certificate_dns_provider: route53
acme_certificate_aws_access_key: REPLACE_WITH_YOUR_ACCESS_KEY
acme_certificate_aws_secret_key: REPLACE_WITH_YOUR_SECRET_KEY
# For DNS challenges with ns1:
# acme_certificate_dns_provider: ns1
# acme_certificate_ns1_secret_key: REPLACE_WITH_YOUR_SECRET_KEY
roles:
- role: acme_certificate
acme_certificate_domains: ['example.com', 'www.example.com']
# Use DNS challenges:
acme_certificate_challenge: dns-01
# The certificate files will be stored at:
# keys/example.com.key (private key)
# keys/example.com.csr (certificate signing request)
# keys/example.com.pem (certificate)
# keys/example.com.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/example.com-chain.pem (intermediate certificate)
# keys/example.com-fullchain.pem (certificate with intermediate certificate)
# keys/example.com-root.pem (root certificate)
# keys/example.com-rootchain.pem (intermediate certificate with root certificate)
- role: acme_certificate
acme_certificate_domains: ['another.example.com']
acme_certificate_key_name: 'another.example.com-rsa'
acme_certificate_key_length: 4096
# Use HTTP challenges:
acme_certificate_challenge: http-01
# The certificate files will be stored at:
# keys/another.example.com-rsa.key (private key)
# keys/another.example.com-rsa.csr (certificate signing request)
# keys/another.example.com-rsa.pem (certificate)
# keys/another.example.com-rsa.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/another.example.com-rsa-chain.pem (intermediate certificate)
# keys/another.example.com-rsa-fullchain.pem (certificate with intermediate certificate)
# keys/another.example.com-rsa-root.pem (root certificate)
# keys/another.example.com-rsa-rootchain.pem (intermediate certificate with root certificate)
- role: acme_certificate
acme_certificate_domains: ['another.example.com']
acme_certificate_key_name: 'another.example.com-ecc'
acme_certificate_algorithm: 'p-256'
# Use HTTP challenges (default for challenge is http-01).
# The certificate files will be stored at:
# keys/another.example.com-ecc.key (private key)
# keys/another.example.com-ecc.csr (certificate signing request)
# keys/another.example.com-ecc.pem (certificate)
# keys/another.example.com-ecc.cnf (OpenSSL config for CSR creation -- can be safely deleted)
# keys/another.example.com-ecc-chain.pem (intermediate certificate)
# keys/another.example.com-ecc-fullchain.pem (certificate with intermediate certificate)
# keys/another.example.com-ecc-root.pem (root certificate)
# keys/another.example.com-ecc-rootchain.pem (intermediate certificate with root certificate)

View File

@@ -0,0 +1,18 @@
---
# Clean up DNS challenges for DNS provider DNSMadeEasy
- name: Cleaning up challenge DNS entries for domains {{ ', '.join(domains) }} via DNSMadeEasy
connection: local
community.general.dnsmadeeasy:
account_key: "{{ dme_account_key }}"
account_secret: "{{ dme_account_secret }}"
domain: "{{ item.key |regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record_ttl: 60
record_type: TXT
record_name: "{{ item.key |regex_replace('^(.*)(\\.[^.]+\\.[^.]+)$', '\\1') }}"
record_value: "{{ item.value|first }}"
state: absent
run_once: True
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,32 @@
---
# Create DNS challenges for DNS provider Amazon Route53
- name: Creating challenge DNS entries for domains {{ ', '.join(domains) }} via DNSMadeEasy
connection: local
community.general.dnsmadeeasy:
account_key: "{{ dme_account_key }}"
account_secret: "{{ dme_account_secret }}"
# This is fragile, and will only work for 2-level domain (eg: corp.com, NOT corp.co.uk )
domain: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record_ttl: 60
record_type: TXT
record_name: "{{ item.key |regex_replace('^(.*)(\\.[^.]+\\.[^.]+)$', '\\1') }}"
record_value: "{{ item.value|first }}"
state: present
# Need dnsmadeeasy module fixed (https://github.com/ansible/ansible/issues/58305)
run_once: True
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs
- name: Wait for DNS entries to become available
shell: "dig txt {{ item.key }} +short @8.8.8.8"
register: dig_result
until: "item.value|first in dig_result.stdout"
retries: 60
delay: 5
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
- name: Pause for 60s for more propagation
pause:
minutes: 1

View File

@@ -0,0 +1,19 @@
---
# Clean up DNS challenges for DNS provider HostTech
- name: Cleaning up challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via HostTech API
hosttech_dns_record:
state: absent
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record: "{{ item.key }}"
type: TXT
ttl: 300
value: "{{ item.value }}"
overwrite: true
hosttech_username: "{{ acme_certificate_hosttech_username }}"
hosttech_password: "{{ acme_certificate_hosttech_password }}"
delegate_to: localhost
run_once: true
with_dict: "{{ acme_certificate_INTERNAL_challenge.get('challenge_data_dns', {}) }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,23 @@
---
# Create DNS challenges for DNS provider HostTech
- name: Creating challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via HostTech API
hosttech_dns_record:
state: present
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record: "{{ item.key }}"
type: TXT
ttl: 300
value: "{{ item.value }}"
overwrite: true
hosttech_username: "{{ acme_certificate_hosttech_username }}"
hosttech_password: "{{ acme_certificate_hosttech_password }}"
delegate_to: localhost
run_once: true
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs
- name: Wait for DNS entries to propagate
pause:
seconds: 10

View File

@@ -0,0 +1,16 @@
---
- name: Cleaning up challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via NS1 API
ns1_record:
apiKey: "{{ acme_certificate_ns1_secret_key }}"
name: "{{ item.key }}"
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
state: absent
type: TXT
answers: []
delegate_to: localhost
run_once: true
when: "'_acme-challenge' in item.key"
with_dict: "{{ acme_certificate_INTERNAL_challenge.get('challenge_data_dns', {}) }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,27 @@
---
- name: Creating challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via NS1 DNS
ns1_record:
apiKey: "{{ acme_certificate_ns1_secret_key }}"
name: "{{ item.key }}"
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
state: present
type: TXT
answers:
- answer:
- "{{ item.value[0] }}"
meta:
up: true
delegate_to: localhost
when: "'_acme-challenge' in item.key"
run_once: true
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
- name: Check if DNS changes propagated at dns1.p01.nsone.net with 10-seconds intervals
command: "dig TXT {{ item.key }} +short @dns1.p01.nsone.net"
register: dig
until: "item.value[0] in dig.stdout"
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
retries: 6
delay: 10
changed_when: false
ignore_errors: yes

View File

@@ -0,0 +1,19 @@
---
# Clean up DNS challenges for DNS provider Amazon Route53
- name: Cleaning up challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via Route53
route53:
state: absent
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record: "{{ item.key }}"
type: TXT
ttl: 60
value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
overwrite: true
aws_access_key: "{{ acme_certificate_aws_access_key }}"
aws_secret_key: "{{ acme_certificate_aws_secret_key }}"
delegate_to: localhost
run_once: true
with_dict: "{{ acme_certificate_INTERNAL_challenge.get('challenge_data_dns', {}) }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,20 @@
---
# Create DNS challenges for DNS provider Amazon Route53
- name: Creating challenge DNS entries for domains {{ ', '.join(acme_certificate_domains) }} via Route53
route53:
state: present
zone: "{{ item.key | regex_replace('^(?:.*\\.|)([^.]+\\.[^.]+)$', '\\1') }}"
record: "{{ item.key }}"
type: TXT
ttl: 60
value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
overwrite: true
aws_access_key: "{{ acme_certificate_aws_access_key }}"
aws_secret_key: "{{ acme_certificate_aws_secret_key }}"
wait: true
delegate_to: localhost
run_once: true
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data_dns }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,15 @@
---
# Clean up challenge files on server.
- name: "Cleaning up challenge files for domains {{ ', '.join(acme_certificate_domains) }}"
file:
path: >-
{{ [
acme_certificate_server_location,
item.value[acme_certificate_challenge].resource[('.well-known/acme-challenge/'|length):]
] | path_join }}"
state: absent
with_dict: "{{ acme_certificate_INTERNAL_challenge.get('acme_certificate_challenge_data', {}) }}"
become: "{{ acme_certificate_http_become }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,31 @@
---
# Create up challenge files directory on server.
- name: Creating challenge destination directory
file:
dest: "{{ acme_certificate_server_location }}"
state: directory
owner: "{{ acme_certificate_http_challenge_user }}"
group: "{{ acme_certificate_http_challenge_group }}"
mode: "{{ acme_certificate_http_challenge_folder_mode }}"
become: "{{ acme_certificate_http_become }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs
# Create challenge files on server.
- name: "Copying challenge files for domains {{ ', '.join(acme_certificate_domains) }}"
copy:
dest: >-
{{ [
acme_certificate_server_location,
item.value[acme_certificate_challenge].resource[('.well-known/acme-challenge/'|length):]
] | path_join }}
content: "{{ item.value[acme_certificate_challenge].resource_value }}"
owner: "{{ acme_certificate_http_challenge_user }}"
group: "{{ acme_certificate_http_challenge_group }}"
mode: "{{ acme_certificate_http_challenge_file_mode }}"
with_dict: "{{ acme_certificate_INTERNAL_challenge.challenge_data }}"
become: "{{ acme_certificate_http_become }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs

View File

@@ -0,0 +1,189 @@
---
- name: Determine whether to force private key regeneration (1/2)
set_fact:
acme_certificate_INTERNAL_force_regenerate_private_key: no
- name: Determine whether to force private key regeneration (2/2)
set_fact:
acme_certificate_INTERNAL_force_regenerate_private_key: yes
tags:
- issue-tls-certs-newkey
- block:
- name: Ansible version check
assert:
that: "ansible_version.string is version('2.8.3', '>=')"
msg: "This version of the acme-certificate role must be used with Ansible 2.8.3 or later."
run_once: yes
- name: Sanity checks
assert:
that: "acme_certificate_challenge != 'dns-01' or acme_certificate_dns_provider is not undefined"
msg: "acme_certificate_dns_provider must be defined for dns-01 DNS challenge"
run_once: yes
- name: "Test whether old certificate files for domains {{ ', '.join(acme_certificate_domains) }} exist"
stat:
path: "{{ [acme_certificate_keys_path, acme_certificate_key_name] | path_join }}.pem"
delegate_to: localhost
register: acme_certificate_INTERNAL_old_certificate_exists
when: "acme_certificate_keys_old_store"
run_once: yes
- name: "Copying old certificate files for domains {{ ', '.join(acme_certificate_domains) }}"
copy:
src: "{{ [acme_certificate_keys_path, acme_certificate_key_name] | path_join }}{{ item }}"
dest: >-
{{ [
acme_certificate_keys_old_path,
(
(ansible_date_time.date ~ '-' ~ ansible_date_time.hour ~ ansible_date_time.minute ~ ansible_date_time.second ~ '-')
if acme_certificate_keys_old_prepend_timestamp else ''
) ~ acme_certificate_key_name ~ item
] | path_join }}
delegate_to: localhost
with_items:
- "-chain.pem"
- "-fullchain.pem"
- "-rootchain.pem"
- "-root.pem"
- ".key"
- ".pem"
when: "acme_certificate_keys_old_store and acme_certificate_INTERNAL_old_certificate_exists.stat.exists"
run_once: yes
tags:
- issue-tls-certs-newkey
- issue-tls-certs
- block:
- name: "Creating private key for domains {{ ', '.join(acme_certificate_domains) }} (RSA)"
openssl_privatekey:
path: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.key'] | path_join }}"
mode: "{{ acme_certificate_privatekey_mode }}"
type: "{{ 'RSA' if acme_certificate_algorithm == 'rsa' else 'ECC' }}"
size: "{{ acme_certificate_key_length if acme_certificate_algorithm == 'rsa' else omit }}"
curve: >-
{{ omit if acme_certificate_algorithm == 'rsa' else
'secp256r1' if acme_certificate_algorithm == 'p-256' else
'secp384r1' if acme_certificate_algorithm == 'p-384' else
'secp521r1' if acme_certificate_algorithm == 'p-521' else
'invalid value for acme_certificate_algorithm!' }}
force: "{{ acme_certificate_INTERNAL_force_regenerate_private_key }}"
delegate_to: localhost
run_once: yes
- name: "Creating CSR for domains {{ ', '.join(acme_certificate_domains) }}"
openssl_csr:
path: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.csr'] | path_join }}"
privatekey_path: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.key'] | path_join }}"
subject_alt_name: |
{{ acme_certificate_domains | map('regex_replace', '^(.*)$', 'DNS:\1' ) | list }}
ocsp_must_staple: "{{ acme_certificate_ocsp_must_staple }}"
use_common_name_for_san: no
force: yes
delegate_to: localhost
run_once: yes
- name: "Get root certificate for domains {{ ', '.join(acme_certificate_domains) }}"
get_url:
url: "{{ acme_certificate_root_certificate }}"
dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-root.pem'] | path_join }}"
force: yes
validate_certs: "{{ acme_certificate_validate_certs }}"
delegate_to: localhost
run_once: yes
- block:
- name: "Preparing challenges for domains {{ ', '.join(acme_certificate_domains) }}"
acme_certificate:
account_key: "{{ acme_certificate_acme_account }}"
modify_account: "{{ acme_certificate_modify_account }}"
csr: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.csr'] | path_join }}"
dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.pem'] | path_join }}"
fullchain_dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-fullchain.pem'] | path_join }}"
chain_dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-chain.pem'] | path_join }}"
account_email: "{{ acme_certificate_acme_email }}"
terms_agreed: "{{ acme_certificate_terms_agreed }}"
challenge: "{{ acme_certificate_challenge }}"
acme_directory: "{{ acme_certificate_acme_directory }}"
acme_version: "{{ acme_certificate_acme_version }}"
force: yes
validate_certs: "{{ acme_certificate_validate_certs }}"
delegate_to: localhost
run_once: yes
register: acme_certificate_INTERNAL_challenge
always:
- debug:
msg: >-
account URI: {{ acme_certificate_INTERNAL_challenge.get('account_uri') }};
order URI: {{ acme_certificate_INTERNAL_challenge.get('order_uri') }}
run_once: yes
- block:
# Set up HTTP challenges
- include_tasks: http-create.yml
when: "acme_certificate_challenge == 'http-01'"
# Set up DNS challenges
- include_tasks: dns-{{ acme_certificate_dns_provider }}-create.yml
when: "acme_certificate_challenge == 'dns-01'"
- name: "Getting certificates for domains {{ ', '.join(acme_certificate_domains) }}"
acme_certificate:
account_key: "{{ acme_certificate_acme_account }}"
modify_account: "{{ acme_certificate_modify_account }}"
csr: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.csr'] | path_join }}"
dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.pem'] | path_join }}"
fullchain_dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-fullchain.pem'] | path_join }}"
chain_dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-chain.pem'] | path_join }}"
account_email: "{{ acme_certificate_acme_email }}"
terms_agreed: "{{ acme_certificate_terms_agreed }}"
challenge: "{{ acme_certificate_challenge }}"
acme_directory: "{{ acme_certificate_acme_directory }}"
acme_version: "{{ acme_certificate_acme_version }}"
force: yes
data: "{{ acme_certificate_INTERNAL_challenge }}"
deactivate_authzs: "{{ acme_certificate_deactivate_authzs }}"
validate_certs: "{{ acme_certificate_validate_certs }}"
delegate_to: localhost
run_once: yes
- name: "Form root chain for domains {{ ', '.join(acme_certificate_domains) }}"
copy:
dest: "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-rootchain.pem'] | path_join }}"
content: |
{{ lookup('file', [acme_certificate_keys_path, acme_certificate_key_name ~ '-root.pem'] | path_join) }}
{{ lookup('file', [acme_certificate_keys_path, acme_certificate_key_name ~ '-chain.pem'] | path_join) }}
delegate_to: localhost
run_once: yes
always:
# Clean up HTTP challenges
- include_tasks: http-cleanup.yml
when: "acme_certificate_challenge == 'http-01'"
# Clean up DNS challenges
- include_tasks: dns-{{ acme_certificate_dns_provider }}-cleanup.yml
when: "acme_certificate_challenge == 'dns-01'"
when: acme_certificate_INTERNAL_challenge is changed
tags:
- issue-tls-certs-newkey
- issue-tls-certs
- name: "Verifying certificate for domains {{ ', '.join(acme_certificate_domains) }}"
command: >-
openssl verify
-CAfile "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-root.pem'] | path_join }}"
-untrusted "{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '-chain.pem'] | path_join }}"
"{{ [acme_certificate_keys_path, acme_certificate_key_name ~ '.pem'] | path_join }}"
changed_when: no
delegate_to: localhost
run_once: yes
ignore_errors: "{{ not acme_certificate_verify_certs }}"
tags:
- issue-tls-certs-newkey
- issue-tls-certs
- verify-tls-certs

View File

@@ -0,0 +1,4 @@
skip_list:
- 'yaml'
- 'role-name'
- 'package-latest'

View File

@@ -0,0 +1,75 @@
---
name: CI
'on':
pull_request:
push:
branches:
- master
schedule:
- cron: "0 7 * * 1"
defaults:
run:
working-directory: 'geerlingguy.gitlab'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Check out the codebase.
uses: actions/checkout@v2
with:
path: 'geerlingguy.gitlab'
- name: Set up Python 3.
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install test dependencies.
run: pip3 install yamllint
- name: Lint code.
run: |
yamllint .
molecule:
name: Molecule
runs-on: ubuntu-latest
strategy:
matrix:
include:
- distro: centos7
playbook: converge.yml
- distro: ubuntu1804
playbook: converge.yml
- distro: debian9
playbook: converge.yml
- distro: centos7
playbook: version.yml
- distro: ubuntu1804
playbook: version.yml
steps:
- name: Check out the codebase.
uses: actions/checkout@v2
with:
path: 'geerlingguy.gitlab'
- name: Set up Python 3.
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install test dependencies.
run: pip3 install ansible molecule[docker] docker
- name: Run Molecule tests.
run: molecule test
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
MOLECULE_DISTRO: ${{ matrix.distro }}
MOLECULE_PLAYBOOK: ${{ matrix.playbook }}

View File

@@ -0,0 +1,38 @@
---
# This workflow requires a GALAXY_API_KEY secret present in the GitHub
# repository or organization.
#
# See: https://github.com/marketplace/actions/publish-ansible-role-to-galaxy
# See: https://github.com/ansible/galaxy/issues/46
name: Release
'on':
push:
tags:
- '*'
defaults:
run:
working-directory: 'geerlingguy.gitlab'
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Check out the codebase.
uses: actions/checkout@v2
with:
path: 'geerlingguy.gitlab'
- name: Set up Python 3.
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install Ansible.
run: pip3 install ansible-base
- name: Trigger a new import on Galaxy.
run: ansible-galaxy role import --api-key ${{ secrets.GALAXY_API_KEY }} $(echo ${{ github.repository }} | cut -d/ -f1) $(echo ${{ github.repository }} | cut -d/ -f2)

View File

@@ -1,2 +1,2 @@
install_date: Tue Apr 20 16:13:49 2021
install_date: Wed Apr 21 16:48:33 2021
version: 3.1.0

View File

@@ -0,0 +1,31 @@
---
- name: Converge
hosts: all
become: true
vars:
gitlab_restart_handler_failed_when: false
pre_tasks:
- name: Update apt cache.
apt: update_cache=true cache_valid_time=600
when: ansible_os_family == 'Debian'
changed_when: false
- name: Remove the .dockerenv file so GitLab Omnibus doesn't get confused.
file:
path: /.dockerenv
state: absent
- name: Set the test GitLab version number for Debian.
set_fact:
gitlab_version: '11.4.0-ce.0'
when: ansible_os_family == 'Debian'
- name: Set the test GitLab version number for RedHat.
set_fact:
gitlab_version: '11.4.0-ce.0.el7'
when: ansible_os_family == 'RedHat'
roles:
- role: geerlingguy.gitlab

View File

@@ -1,2 +1,2 @@
install_date: Tue Apr 20 16:13:48 2021
install_date: Wed Apr 21 16:48:32 2021
version: 1.10.0

View File

@@ -1,2 +1,2 @@
install_date: Tue Apr 20 16:13:55 2021
install_date: Wed Apr 21 16:48:44 2021
version: master

View File

@@ -0,0 +1,12 @@
---
extends: default
rules:
# 80 chars should be enough, but don't fail if a line is longer
line-length:
max: 80
level: warning
indentation:
spaces: 2
indent-sequences: whatever

View File

@@ -1,2 +1,2 @@
install_date: Tue Apr 20 16:13:54 2021
install_date: Wed Apr 21 16:48:42 2021
version: 2.1.0

View File

@@ -0,0 +1,3 @@
---
collections:
- containers.podman

View File

@@ -0,0 +1,37 @@
---
- name: check if user is in subuid file
find:
path: /etc/subuid
contains: '^{{ container_run_as_user }}:.*$'
register: uid_line_found
when: container_run_as_user != 'root'
- name: check if group is in subgid file
find:
path: /etc/subgid
contains: '^{{ container_run_as_group }}:.*$'
register: gid_line_found
when: container_run_as_group != 'root'
- name: ensure user is in subuid file, if it was missing
lineinfile:
path: /etc/subuid
regexp: "^{{ container_run_as_user }}:.*"
line: "{{ container_run_as_user }}:165536:65536"
create: true
mode: '0644'
owner: root
group: root
when: container_run_as_user != 'root' and not uid_line_found.matched
- name: ensure group is in subgid file, if it was missing
lineinfile:
path: /etc/subgid
regexp: "^{{ container_run_as_group }}:.*"
line: "{{ container_run_as_group }}:165536:65536"
create: true
mode: '0644'
owner: root
group: root
when: container_run_as_group != 'root' and not gid_line_found.matched

View File

@@ -0,0 +1,12 @@
---
skip_list:
- '106' # Role name does not match ^[a-z][a-z0-9_]+$ pattern
- '206' # Variables should have spaces before and after: {{ var_name }}
- '208' # File permissions unset or incorrect
- '301' # Commands should not change things if nothing needs doing
- '303' # Using command rather than module
- '305' # Use shell only when shell functionality is required
- '403' # Package installs should not use latest
- '502' # All tasks should be named
- '601' # Don't compare to literal True/False
- '602' # Don't compare to empty string

View File

@@ -0,0 +1,12 @@
---
# Configuration for probot-stale - https://github.com/probot/stale
daysUntilStale: 30
daysUntilClose: 14
staleLabel: stale
markComment: >
Thank you for your contribution! There was no activity in this pull request
recently. To avoid pull requests to pile up, an automated process marked this
pull request as stale. It will close this pull request if no further activity
occurs. The current policy is available at:
https://github.com//linux-system-roles/network/blob/main/.github/stale.yml
only: pulls

View File

@@ -0,0 +1,11 @@
---
name: markdownlint
on: [push, pull_request]
jobs:
markdownlint:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@main
- name: Run mdl
uses: actionshub/markdownlint@main

View File

@@ -0,0 +1,51 @@
# yamllint disable rule:line-length
name: tox
on: # yamllint disable-line rule:truthy
- pull_request
env:
TOX_LSR: "git+https://github.com/linux-system-roles/tox-lsr@2.4.0"
LSR_ANSIBLE_TEST_DOCKER: "true"
LSR_ANSIBLES: 'ansible==2.8.* ansible==2.9.*'
LSR_MSCENARIOS: default
# LSR_EXTRA_PACKAGES: "libdbus-1-dev libgirepository1.0-dev python3-dev"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
python:
runs-on: ubuntu-latest
strategy:
matrix:
pyver: ['2.7', '3.6', '3.7', '3.8']
steps:
- name: checkout PR
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.pyver }}
- name: Install platform dependencies, python, tox, tox-lsr
run: |
set -euxo pipefail
python -m pip install --upgrade pip
sudo apt-get update
sudo apt-get install git
pip install "$TOX_LSR"
lsr_ci_preinstall
- name: Run tox tests
run: |
set -euxo pipefail
toxpyver=$(echo "${{ matrix.pyver }}" | tr -d .)
toxenvs="py${toxpyver}"
case "$toxpyver" in
27) toxenvs="${toxenvs},coveralls,flake8,pylint" ;;
36) toxenvs="${toxenvs},coveralls,black,yamllint,ansible-lint,collection" ;;
37) toxenvs="${toxenvs},coveralls" ;;
38) toxenvs="${toxenvs},coveralls" ;;
esac
TOXENV="$toxenvs" lsr_ci_runtox
python-26:
runs-on: ubuntu-latest
steps:
- name: checkout PR
uses: actions/checkout@v2
- name: Run py26 tests
uses: linux-system-roles/lsr-gh-action-py26@1.0.1

View File

@@ -0,0 +1,5 @@
---
extraction:
python:
python_setup:
version: 2

View File

@@ -0,0 +1,9 @@
all
# https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md003---header-style
rule 'MD003', :style => :setext_with_atx
# https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md013---line-length
rule 'MD013', :line_length => 88
# https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md029---ordered-list-item-prefix
rule 'MD029', :style => :ordered
# https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md#md024---multiple-headers-with-the-same-content
rule "MD024", :allow_different_nesting => true

View File

@@ -0,0 +1 @@
style '.mdl_style.rb'

View File

@@ -0,0 +1,11 @@
#!/bin/bash
# SPDX-License-Identifier: MIT
set -e
. "$LSR_SCRIPTDIR/utils.sh"
# Write your custom commands here that should be run when `tox -e custom`:
if lsr_check_python_version python -eq '3.6'; then
(set -x; cd "${TOPDIR}/tests"; python ./ensure_provider_tests.py)
fi

View File

@@ -0,0 +1,23 @@
# SPDX-License-Identifier: MIT
---
extends: yamllint_defaults.yml
# possible customizations over the base yamllint config
# skip the yaml files in the /tests/ directory
# NOTE: If you want to customize `ignore` you'll have to
# copy in all of the config from .yamllint.yml, then
# add your own - so if you want to just add /tests/ to
# be ignored, you'll have to add the ignores from the base
# ignore: |
# /tests/
# /.tox/
# skip checking line length
# NOTE: the above does not apply to `rules` - you do not
# have to copy all of the rules from the base config
# rules:
# line-length: disable
rules:
truthy: disable
line-length:
ignore: |
/tests/tests_wireless_plugin_installation_nm.yml
/tests/tests_team_plugin_installation_nm.yml

View File

@@ -0,0 +1,49 @@
Changelog
=========
[1.3.0] - 2021-04-08
--------------------
### Changes
- Use inclusive language
- `slave` is deprecated in favor of `port`
- `master` is deprecated in favor of `controller`
### New features
- Support disabling IPv6
- Support `dns_options` when using one or more IPv4 nameservers
- Support Ethtool coalesce settings
- Support dummy interfaces
### Bug fixes
- Fix static IPv6 support for initscripts provider
[1.2.0] - 2020-08-26
--------------------
### Changes
- Rename ethtool features to use underscores instead of dashes to support
Jinja2 dot notation. Accept old notation for compatibility with existing
playbooks.
### New features
- Initial 802.1x authentication support (only EAP-TLS)
- Wireless support
- Handle OracleLinux as a RHEL clone
- Remove dependency on ethtool command line tool
- initscripts: Support creating and activating bond profiles in one run
- Ignore up/down states if a profile is not defined and not present on the
managed host
- Document bond profiles
### Bug fixes
- NetworkManager: Always rollback checkpoint on failure
- NetworkManager: Try to reapply changes to reduce network interruptions
- initscripts: Fix dependencies for Fedora 32
- Only log actual warnings as Ansible warnings

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: MIT
# ansible and dependencies for all supported platforms
ansible ; python_version > "2.6"
ansible<2.7 ; python_version < "2.7"
idna<2.8 ; python_version < "2.7"
PyYAML<5.1 ; python_version < "2.7"

View File

@@ -0,0 +1,409 @@
Contributing to the Network Linux System Role
=============================================
Where to start
--------------
- **Bugs and needed implementations** are listed on [Github
Issues](https://github.com/linux-system-roles/network/issues). Issues labeled with
[**help
wanted**](https://github.com/linux-system-roles/network/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
are likely to be suitable for new contributors!
- **Code** is managed on
[Github](https://github.com/linux-system-roles/network), using [Pull
Requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
- The code needs to be **compatible with Python 2.6, 2.7, 3.6, 3.7 and 3.8**.
Code structure
--------------
The repository is structured as follows:
- `./defaults/` - Contains the default role configuration.
- `./examples/` - Contains YAML examples for different configurations.
- `./library/network_connections.py` - Contains the internal Ansible module, which is
the main script. It controls the communication between the role and Ansible, imports
the YAML configuration and applies the changes to the provider (i.e. NetworkManager,
initscripts).
- `./meta/` - Metadata of the project.
- `./module_utils/network_lsr/` - Contains other files that are useful for the network
role (e.g. the YAML argument validator)
- `./tasks/` - Declaration of the different tasks that the role is going to execute.
- `./tests/playbooks/` - Contains the complete tests for the role. `./tests/test_*.yml`
are shims to run tests once for every provider. `./tests/tasks/` contains task
snippets that are used in multiple tests to avoid having the same code repeated
multiple times.
The rest of files in the root folder mostly serve as configuration files for diferent
testing tools and bots that help with the manteinance of the project.
The code files will always have the imports on the first place, followed by constants
and in the last place, classes and methods. The style of python coding for this project
is [**PEP 8**](https://www.python.org/dev/peps/pep-0008/), with automatic formatting
thanks to [Python Black](https://black.readthedocs.io/en/stable/). Make sure to install
the formatter, it will help you a lot throughout the whole coding process!
Configuring Git
---------------
Before starting to contribute, make sure you have the basic git configuration: Your name
and email. This will be useful when signing your contributions. The following commands
will set your global name and email, althought you can change it later for every repo:
```bash
git config --global user.name "Jane Doe"
git config --global user.email janedoe@example.com
```
The git editor is your system's default. If you feel more comfortable with a different
editor for writing your commits (such as Vim), change it with:
```bash
git config --global core.editor vim
```
If you want to check your settings, use `git config --list` to see all the settings Git
can find.
How to contribute
-----------------
1. Make a
[fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo)
of this repository.
2. Create a new git branch on your local fork (the name is not relevant) and make the
changes you need to complete an issue.
3. Do not forget to run unit and integration tests before pushing any changes!
1. This project uses [tox](https://tox.readthedocs.io/en/latest/) to run unit tests.
You can try it with `tox -e py36` in case you want to try it using Python 3.6, or
just `tox` if you want to run all the tests.
2. Check the formatting of the code with
[Python Black](https://black.readthedocs.io/en/stable/)
3. Check the YAML files are correctly formatted using `tox -e yamllint`.
4. Integration tests are executed as
[Ansible Playbooks](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html).
To run them you can use a cloud image like the [CentOS Linux 8.1
VM](https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2)
and execute the command and download the package
`standard-test-roles-inventory-qemu` from the Fedora repository:
```bash
dnf install standard-test-roles-inventory-qemu
```
Note that the last path is the one of the test you want to run:
```bash
TEST_SUBJECTS=CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2 \
ansible-playbook -v -i /usr/share/ansible/inventory/standard-inventory-qcow2 \
tests/test_default.yml
```
5. Check the markdown format with
[mdl](https://github.com/markdownlint/markdownlint) after changing any
markdown document.
4. Once the work is ready and commited, push the branch to your remote fork and click on
"new Pull Request" on Github.
5. All set! Now wait for the continuous integration to pass and go over your commit if
there are any errors. If there is no problem with your contribution, the mantainer
will merge it to the main project.
### Find other images for testing
The CentOS project publishes cloud images for
[CentOS Linux 6](https://cloud.centos.org/centos/6/images/),
[CentOS Linux 7](https://cloud.centos.org/centos/7/images/) and
[CentOS Linux 8](https://cloud.centos.org/centos/8/x86_64/images/).
- For qemu testing cases, we prefer the image architecture to be `x86_64-GenericCloud`.
- `2003` in `CentOS-7-x86_64-GenericCloud-2003.qcow2c` stands for image released in
March 2020.
- We can use the image with extension `.qcow2` and `.qcow2c`.
- To save the image, right click on the link above, then select "Save link as...".
For Fedora, we recommend to use the [latest qcow2
images](https://kojipkgs.fedoraproject.org/compose/cloud/).
### Some important tips
- Make sure your fork and branch are up-to-date with the main project. First of all,
[configure a remote upstream for your
fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork),
and keep your branch up-to-date with the upstream using
`git pull --rebase upstream main`.
- Try to make a commit per issue.
- If you are asked to make changes to your PR, don't panic! Many times it is enough to
amend your previous commit adding the new content to it (`git commit --amend`). Be
sure to pull the latest upstream changes after that, and use `git push
--force-with-lease` to re-upload your commit with the changes! Another way of doing
changes to a PR is by [squashing
commits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-merges#squash-and-merge-your-pull-request-commits).
- There are times when someone has made changes on a file you were modifying while you
were making changes to your unfinished commit. At times like this, you need to make a
[**rebase**](https://help.github.com/en/github/using-git/about-git-rebase) with
conflicts. On the rebase you have to compare what the other person added to what you
added, and merge both file versions into one that combines it all.
- If you have any doubt, do not hesitate to ask! You can join IRC channel \#systemroles
on freenode, or ask on the PR/issue itself.
### Naming Ansible Items
- All YAML or Python files, variables, arguments, repositories, and other such names
should follow standard Python naming conventions of being in
`snake_case_naming_schemes`.
- Names should be mnemonic and descriptive and not strive to shorten more than
necessary. Systems support long identifier names, so use them to be descriptive
- All defaults and all arguments to a role should have a name that begins with the role
name to help avoid collision with other names. Avoid names like `packages` in favor of
a name like `network_packages`.
- Same argument applies for modules provided in the roles, they also need a `$ROLENAME_`
prefix: `network_module`. While they are usually implementation details and not intended
for direct use in playbooks, the unfortunate fact is that importing a role makes them
available to the rest of the playbook and therefore creates opportunities for name
collisions.
- Moreover, internal variables (those that are not expected to be set by users) are to
be prefixed by two underscores: `__network_variable`. This includes variables set by
`set_fact` and `register`, because they persist in the namespace after the role has
finished!
- Do not use special characters other than underscore in variable names, even if
YAML/JSON allow them. (Using such variables in Jinja2 or Python would be then very
confusing and probably not functional.)
*Find more explanation on this matter in the [meta
standards](https://github.com/oasis-roles/meta_standards#naming-things).*
### Write a good commit message
Here are a few rules to keep in mind while writing a commit message
1. Separate subject from body with a blank line
2. Limit the subject line to 50 characters
3. Capitalize the subject line
4. Do not end the subject line with a period
5. Use the imperative mood in the subject line
6. Wrap the body at 72 characters
7. Use the body to explain what and why vs. how
A good commit message looks something like this:
```text
Summarize changes in around 50 characters or less
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.
Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here's the place to explain them.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between, but conventions
vary here
If you use an issue tracker, put references to them at the bottom,
like this:
Resolves: #123
See also: #456, #789
Do not forget to sign your commit! Use `git commit -s`
```
This is taken from [chris beams git commit](https://chris.beams.io/posts/git-commit/).
You may want to read this for a more detailed explanation (and links to other posts on
how to write a good commit message). This content is licensed under
[CC-BY-SA](https://creativecommons.org/licenses/by-sa/4.0/).
### Sign off your commit
You need to sign off your commit. By adding your 'Signed-off-by' line to the commit
messages you adhere to the
[Developer Certificate of Origin (DCO)](https://developercertificate.org/).
Use the `-s` command-line option to append the `Signed-off-by` line when committing your
code:
`$ git commit -s`
This is the full text of the Developer Certificate of Origin:
```text
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
### Debugging
When using the `nm` provider, NetworkManager create a checkpoint and reverts the changes
on failures. This makes it hard to debug the error. To disable this, set the Ansible
variable `__network_debug_flags` to include the value `disable-checkpoints`. Also tests
clean up by default in case there are failures. They should be tagged as
`tests::cleanup` and can be skipped. To use both, run the test playbooks like this:
```bash
ansible-playbook --skip-tags tests::cleanup \
-e "__network_debug_flags=disable-checkpoints" \
-i testhost, tests/playbooks/tests_802_1x.yml
```
### NetworkManager Documentation
- [NM 1.0](https://lazka.github.io/pgi-docs/#NM-1.0), it contains a full explanation
about the NetworkManager API.
### Integration tests with podman
1. Create `~/.ansible/collections/ansible_collections/containers/podman/` if this
directory does not exist and `cd` into this directory.
```bash
mkdir -p ~/.ansible/collections/ansible_collections/containers/podman/
cd ~/.ansible/collections/ansible_collections/containers/podman/
```
2. Clone the collection plugins for Ansible-Podman into the current directory.
```bash
git clone https://github.com/containers/ansible-podman-collections.git .
```
3. Change directory into the `tests` subdirectory.
```bash
cd ~/network/tests
```
4. Use podman with `-d` to run in the background (daemon). Use `c7` because
`centos/systemd` is centos7.
```bash
podman run --name lsr-ci-c7 --rm --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-d registry.centos.org/centos/systemd
```
5. Use `podman unshare` first to run "podman mount" in root mode, use `-vi` to run
ansible as inventory in verbose mode, use `-c podman` to use collection plugins. Note,
the following tests are currently not working with podman:
- `tests_802_1x_nm.yml`
- `tests_802_1x_updated_nm.yml`
- `tests_bond_initscripts.yml`
- `tests_bond_nm.yml`
- `tests_bridge_initscripts.yml`
- `tests_bridge_nm.yml`
- `tests_default_nm.yml`
- `tests_ethernet_nm.yml`
- `tests_reapply_nm.yml`
- `tests_states_nm.yml`
- `tests_vlan_mtu_initscripts.yml`
- `tests_vlan_mtu_nm.yml`
- `tests_wireless_nm.yml`
```bash
podman unshare
ansible-playbook -vi lsr-ci-c7, -c podman tests_provider_nm.yml
```
6. NOTE that this leaves the container running in the background, to kill it:
```bash
podman stop lsr-ci-c7
podman rm lsr-ci-c7
```
### Continuous integration
The [continuous integration](https://en.wikipedia.org/wiki/Continuous_integration) (CI)
contains a set of automated tests that are triggered on a remote server. Some of them
are immediately triggered when pushing new content to a PR (i.e. the tests hosted on
TravisCI) while other need to be triggered by members of the project. This second
set of tests can be manually triggered. To trigger them, write a command as a PR
comment. The available commands are:
- [citest] - Trigger a re-test for all machines.
- [citest bad] - Trigger a re-test for all machines with an error or failure status.
- [citest pending] - Trigger a re-test for all machines with a pending status.
- [citest commit:<sha1\>] - Whitelist a commit to be tested if the submitter is not
trusted.
How to reach us
---------------
The mailing list for developers: systemroles@lists.fedorahosted.org
[Subscribe to the mailing list](https://lists.fedorahosted.org/admin/lists/systemroles.lists.fedorahosted.org/)
[Archive of the mailing list](https://lists.fedorahosted.org/archives/list/systemroles@lists.fedorahosted.org/)
If you are using IRC, join the `#systemroles` IRC channel on
[freenode](https://freenode.net/kb/answer/chat)
*Thanks for contributing and happy coding!!*

View File

@@ -0,0 +1,4 @@
# SPDX-License-Identifier: MIT
# Write requirements for running your custom commands in tox here:
PyYAML; python_version == '2.7' or python_version >= '3.5'

View File

@@ -0,0 +1,36 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Specify the bond profile
- name: bond0
state: up
type: bond
interface_name: bond0
# ip configuration (optional)
ip:
address:
- "192.0.2.24/24"
- "2001:db8::23/64"
# bond configuration settings: (optional)
bond:
mode: active-backup
miimon: 110
# add an ethernet profile to the bond
- name: member1
state: up
type: ethernet
interface_name: eth1
controller: bond0
# add a second ethernet profile to the bond
- name: member2
state: up
type: ethernet
interface_name: eth2
controller: bond0
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1,38 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a bond profile, which is the parent of VLAN.
- name: prod2
state: up
type: bond
interface_name: bond2
ip:
dhcp4: no
auto6: no
bond:
mode: active-backup
miimon: 110
# set an ethernet as port to the bond
- name: prod2-port1
state: up
type: ethernet
interface_name: "{{ network_interface_name2 }}"
controller: prod2
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,36 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a bridge profile, which is the parent of VLAN.
- name: prod2
state: up
type: bridge
interface_name: bridge2
ip:
dhcp4: no
auto6: no
# set an ethernet port to the bridge
- name: prod2-port1
state: up
type: ethernet
interface_name: "{{ network_interface_name2 }}"
controller: prod2
port_type: bridge
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1 @@
../tests/playbooks/down_profile.yml

View File

@@ -0,0 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
network_connections:
# Specify the dummy profile
- name: dummy0
state: up
type: dummy
interface_name: dummy0
ip:
address:
- "192.0.2.42/30"
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1,44 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
network_connections:
- name: eth0
type: ethernet
ip:
route_metric4: 100
dhcp4: no
gateway4: 192.0.2.1
dns:
- 192.0.2.2
- 198.51.100.5
dns_search:
- example.com
- subdomain.example.com
dns_options:
- rotate
- timeout:1
route_metric6: -1
auto6: no
gateway6: 2001:db8::1
address:
- 192.0.2.3/24
- 198.51.100.3/26
- 2001:db8::80/7
route:
- network: 198.51.100.128
prefix: 26
gateway: 198.51.100.1
metric: 2
- network: 198.51.100.64
prefix: 26
gateway: 198.51.100.6
metric: 4
route_append_only: no
rule_append_only: yes
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1,18 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create one ethernet profile and activate it.
# The profile uses automatic IP addressing
# and is tied to the interface by MAC address.
- name: prod1
state: up
type: ethernet
autoconnect: yes
mac: "{{ network_mac1 }}"
mtu: 1450
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,30 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
- name: eth0
type: ethernet
ieee802_1x:
identity: myhost
eap: tls
private_key: /etc/pki/tls/client.key
# recommend vault encrypting the private key password
# see https://docs.ansible.com/ansible/latest/user_guide/vault.html
private_key_password: "p@55w0rD"
client_cert: /etc/pki/tls/client.pem
ca_cert: /etc/pki/tls/cacert.pem
domain_suffix_match: example.com
# certs have to be deployed first
pre_tasks:
- name: copy certs/keys for 802.1x auth
copy:
src: "{{ item }}"
dest: "/etc/pki/tls/{{ item }}"
with_items:
- client.key
- client.pem
- cacert.pem
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,29 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a profile for the underlying device of the VLAN.
- name: prod2
type: ethernet
autoconnect: no
state: up
interface_name: "{{ network_interface_name2 }}"
ip:
dhcp4: no
auto6: no
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,38 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ network_interface_name1 }}"
state: up
type: ethernet
ip:
dhcp4: no
auto6: no
ethtool:
coalesce:
adaptive_rx: yes
adaptive_tx: no
pkt_rate_high: 128
pkt_rate_low: 128
rx_frames: 128
rx_frames_high: 128
rx_frames_irq: 128
rx_frames_low: 128
rx_usecs: 128
rx_usecs_high: 128
rx_usecs_irq: 128
rx_usecs_low: 128
sample_interval: 128
stats_block_usecs: 128
tx_frames: 128
tx_frames_high: 128
tx_frames_irq: 128
tx_frames_low: 128
tx_usecs: 128
tx_usecs_high: 128
tx_usecs_irq: 128
tx_usecs_low: 128

View File

@@ -0,0 +1,19 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ network_interface_name1 }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"
ethtool:
features:
gro: "no"
gso: "yes"
tx_sctp_segmentation: "no"

View File

@@ -0,0 +1,14 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ network_interface_name1 }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"

View File

@@ -0,0 +1,12 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
network_connections:
- name: eth0
type: ethernet
ip:
ipv6_disabled: true
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1,12 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Set {{ profile }} down
hosts: all
vars:
network_connections:
- name: "{{ profile }}"
persistent_state: absent
state: down
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1 @@
../tests/playbooks/remove_profile.yml

View File

@@ -0,0 +1,33 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Specify the team profile
- name: team0
state: up
type: team
interface_name: team0
# ip configuration (optional)
ip:
address:
- "192.0.2.24/24"
- "2001:db8::23/64"
# add an team profile to the team
- name: member1
state: up
type: ethernet
interface_name: eth1
controller: team0
# add a second team profile to the team
- name: member2
state: up
type: ethernet
interface_name: eth2
controller: team0
roles:
- linux-system-roles.network
...

View File

@@ -0,0 +1,15 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
- name: wlan0
type: wireless
wireless:
ssid: "My WPA2-PSK Network"
key_mgmt: "wpa-psk"
# recommend vault encrypting the wireless password
# see https://docs.ansible.com/ansible/latest/user_guide/vault.html
password: "p@55w0rD"
roles:
- linux-system-roles.network

View File

@@ -1,2 +1,2 @@
install_date: Tue Apr 20 16:13:56 2021
install_date: Wed Apr 21 16:48:45 2021
version: 1.3.0

View File

@@ -0,0 +1,56 @@
# SPDX-License-Identifier: BSD-3-Clause
import array
import struct
import fcntl
import socket
from .utils import Util
ETHTOOL_GPERMADDR = 0x00000020
SIOCETHTOOL = 0x8946
MAX_ADDR_LEN = 32
IFNAMESIZ = 16
def get_perm_addr(ifname):
"""
Return the Permanent address value for the specified interface using the
ETHTOOL_GPERMADDR ioctl command.
Please for further documentation, see:
https://github.com/torvalds/linux/blob/master/include/uapi/linux/ethtool.h#L734
https://github.com/torvalds/linux/blob/master/include/uapi/linux/ethtool.h#L1388
https://git.kernel.org/pub/scm/network/ethtool/ethtool.git/tree/ethtool.c#n4172
"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockfd = sock.fileno()
ifname = ifname.encode("utf-8")
if len(ifname) > IFNAMESIZ:
return None
ecmd = array.array(
"B",
struct.pack(
"II%is" % MAX_ADDR_LEN,
ETHTOOL_GPERMADDR,
MAX_ADDR_LEN,
b"\x00" * MAX_ADDR_LEN,
),
)
ifreq = struct.pack("%isP" % IFNAMESIZ, ifname, ecmd.buffer_info()[0])
fcntl.ioctl(sockfd, SIOCETHTOOL, ifreq)
try:
res = ecmd.tobytes()
except AttributeError: # tobytes() is not available in python2
res = ecmd.tostring()
_, size, perm_addr = struct.unpack("II%is" % MAX_ADDR_LEN, res)
perm_addr = Util.mac_ntoa(perm_addr[:size])
except IOError:
perm_addr = None
finally:
sock.close()
return perm_addr

View File

@@ -0,0 +1,7 @@
# Relative import is not support by ansible 2.8 yet
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.nm import provider # noqa:E501
# pylint: enable=import-error, no-name-in-module
provider.NetworkManagerProvider

View File

@@ -0,0 +1,128 @@
# SPDX-License-Identifier: BSD-3-Clause
# Handle NM.ActiveConnection
import logging
# Relative import is not support by ansible 2.8 yet
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.nm import client # noqa:E501
from ansible.module_utils.network_lsr.nm import error # noqa:E501
# pylint: enable=import-error, no-name-in-module
NM_AC_STATE_CHANGED_SIGNAL = "state-changed"
def deactivate_active_connection(nm_ac, timeout, check_mode):
if not nm_ac or nm_ac.props.state == client.NM.ActiveConnectionState.DEACTIVATED:
logging.info("Connection is not active, no need to deactivate")
return False
if not check_mode:
main_loop = client.get_mainloop(timeout)
logging.debug(
"Deactivating {id} with timeout {timeout}".format(
id=nm_ac.get_id(), timeout=timeout
)
)
user_data = main_loop
handler_id = nm_ac.connect(
NM_AC_STATE_CHANGED_SIGNAL, _nm_ac_state_change_callback, user_data
)
logging.debug(
"Registered {signal} on client.NM.ActiveConnection {id}".format(
signal=NM_AC_STATE_CHANGED_SIGNAL, id=nm_ac.get_id()
)
)
if nm_ac.props.state != client.NM.ActiveConnectionState.DEACTIVATING:
nm_client = client.get_client()
user_data = (main_loop, nm_ac, nm_ac.get_id(), handler_id)
nm_client.deactivate_connection_async(
nm_ac,
main_loop.cancellable,
_nm_ac_deactivate_call_back,
user_data,
)
logging.debug(
"Deactivating client.NM.ActiveConnection {0}".format(nm_ac.get_id())
)
main_loop.run()
return True
def _nm_ac_state_change_callback(nm_ac, state, reason, user_data):
main_loop = user_data
if main_loop.is_cancelled:
return
logging.debug(
"Got client.NM.ActiveConnection state change: {id}: {state} {reason}".format(
id=nm_ac.get_id(), state=state, reason=reason
)
)
if nm_ac.props.state == client.NM.ActiveConnectionState.DEACTIVATED:
logging.debug(
"client.NM.ActiveConnection {0} is deactivated".format(nm_ac.get_id())
)
main_loop.quit()
def _nm_ac_deactivate_call_back(nm_client, result, user_data):
main_loop, nm_ac, nm_ac_id, handler_id = user_data
logging.debug("client.NM.ActiveConnection deactivating callback")
if main_loop.is_cancelled:
if nm_ac:
nm_ac.handler_disconnect(handler_id)
return
try:
success = nm_client.deactivate_connection_finish(result)
except client.GLib.Error as e:
if e.matches(
client.NM.ManagerError.quark(), client.NM.ManagerError.CONNECTIONNOTACTIVE
):
logging.info(
"Connection is not active on {0}, no need to deactivate".format(
nm_ac_id
)
)
if nm_ac:
nm_ac.handler_disconnect(handler_id)
main_loop.quit()
return
else:
_deactivate_fail(
main_loop,
handler_id,
nm_ac,
"Failed to deactivate connection {id}, error={error}".format(
id=nm_ac_id, error=e
),
)
return
except Exception as e:
_deactivate_fail(
main_loop,
handler_id,
nm_ac,
"Failed to deactivate connection {id}, error={error}".format(
id=nm_ac_id, error=e
),
)
return
if not success:
_deactivate_fail(
main_loop,
handler_id,
nm_ac,
"Failed to deactivate connection {0}, error='None "
"returned from deactivate_connection_finish()'".format(nm_ac_id),
)
def _deactivate_fail(main_loop, handler_id, nm_ac, msg):
if nm_ac:
nm_ac.handler_disconnect(handler_id)
logging.error(msg)
main_loop.fail(error.LsrNetworkNmError(msg))

View File

@@ -0,0 +1,96 @@
# SPDX-License-Identifier: BSD-3-Clause
import logging
# Relative import is not support by ansible 2.8 yet
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.nm import error # noqa:E501
import gi
try:
gi.require_version("NM", "1.0")
# It is required to state the NM version before importing it
# But this break the flake8 rule: https://www.flake8rules.com/rules/E402.html
# Use NOQA: E402 to suppress it.
from gi.repository import NM # NOQA: E402
from gi.repository import GLib # NOQA: E402
from gi.repository import Gio # NOQA: E402
# pylint: enable=import-error, no-name-in-module
NM
GLib
Gio
except ValueError:
# This is to workaround a bug in ansible 2.9 which causes
# this code to be executed on the control node, where NM
# is not guaranteed to exist. On the other hand, it is
# ensured on the managed nodes as NM package is installed
# in the network role. Therefore, this exception handling
# does not affect the network installation and configuration
# on the managed nodes.
pass
def get_client():
return NM.Client.new()
class _NmMainLoop(object):
def __init__(self, timeout):
self._mainloop = GLib.MainLoop()
self._cancellable = Gio.Cancellable.new()
self._timeout = timeout
self._timeout_id = None
def run(self):
logging.debug("NM mainloop running")
user_data = None
self._timeout_id = GLib.timeout_add(
int(self._timeout * 1000),
self._timeout_call_back,
user_data,
)
logging.debug("Added timeout checker")
self._mainloop.run()
def _timeout_call_back(self, _user_data):
logging.error("Timeout")
self.fail(error.LsrNetworkNmError("Timeout"))
@property
def cancellable(self):
return self._cancellable
@property
def is_cancelled(self):
if self._cancellable:
return self._cancellable.is_cancelled()
return True
def _clean_up(self):
logging.debug("NM mainloop cleaning up")
if self._timeout_id:
logging.debug("Removing timeout checker")
GLib.source_remove(self._timeout_id)
self._timeout_id = None
if self._cancellable:
logging.debug("Canceling all pending tasks")
self._cancellable.cancel()
self._cancellable = None
self._mainloop = None
def quit(self):
logging.debug("NM mainloop quiting")
self._mainloop.quit()
self._clean_up()
def fail(self, exception):
self.quit()
raise exception
def get_mainloop(timeout):
return _NmMainLoop(timeout)

View File

@@ -0,0 +1,113 @@
# SPDX-License-Identifier: BSD-3-Clause
# Handle NM.RemoteConnection
import logging
# Relative import is not support by ansible 2.8 yet
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.nm import client # noqa:E501
from ansible.module_utils.network_lsr.nm import error # noqa:E501
# pylint: enable=import-error, no-name-in-module
def delete_remote_connection(nm_profile, timeout, check_mode):
if not nm_profile:
logging.info("NULL NM.RemoteConnection, no need to delete")
return False
if not check_mode:
main_loop = client.get_mainloop(timeout)
user_data = main_loop
nm_profile.delete_async(
main_loop.cancellable,
_nm_profile_delete_call_back,
user_data,
)
logging.debug(
"Deleting profile {id}/{uuid} with timeout {timeout}".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid(), timeout=timeout
)
)
main_loop.run()
return True
def _nm_profile_delete_call_back(nm_profile, result, user_data):
main_loop = user_data
if main_loop.is_cancelled:
return
try:
success = nm_profile.delete_finish(result)
except Exception as e:
main_loop.fail(
error.LsrNetworkNmError(
"Connection deletion aborted on {id}/{uuid}: error={error}".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid(), error=e
)
)
)
if success:
main_loop.quit()
else:
main_loop.fail(
error.LsrNetworkNmError(
"Connection deletion aborted on {id}/{uuid}: error=unknown".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid()
)
)
)
def volatilize_remote_connection(nm_profile, timeout, check_mode):
if not nm_profile:
logging.info("NULL NM.RemoteConnection, no need to volatilize")
return False
if not check_mode:
main_loop = client.get_mainloop(timeout)
user_data = main_loop
nm_profile.update2(
None, # settings
client.NM.SettingsUpdate2Flags.IN_MEMORY_ONLY
| client.NM.SettingsUpdate2Flags.VOLATILE,
None, # args
main_loop.cancellable,
_nm_profile_volatile_update2_call_back,
user_data,
)
logging.debug(
"Volatilizing profile {id}/{uuid} with timeout {timeout}".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid(), timeout=timeout
)
)
main_loop.run()
return True
def _nm_profile_volatile_update2_call_back(nm_profile, result, user_data):
main_loop = user_data
if main_loop.is_cancelled:
return
try:
success = nm_profile.update2_finish(result)
except Exception as e:
main_loop.fail(
error.LsrNetworkNmError(
"Connection volatilize aborted on {id}/{uuid}: error={error}".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid(), error=e
)
)
)
if success:
main_loop.quit()
else:
main_loop.fail(
error.LsrNetworkNmError(
"Connection volatilize aborted on {id}/{uuid}: error=unknown".format(
id=nm_profile.get_id(), uuid=nm_profile.get_uuid()
)
)
)

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: BSD-3-Clause
class LsrNetworkNmError(Exception):
pass

View File

@@ -0,0 +1,58 @@
# SPDX-License-Identifier: BSD-3-Clause
import logging
# Relative import is not support by ansible 2.8 yet
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.nm import active_connection # noqa:E501
from ansible.module_utils.network_lsr.nm import client # noqa:E501
from ansible.module_utils.network_lsr.nm import connection # noqa:E501
# pylint: enable=import-error, no-name-in-module
class NetworkManagerProvider:
def deactivate_connection(self, connection_name, timeout, check_mode):
"""
Return True if changed.
"""
nm_client = client.get_client()
changed = False
for nm_ac in nm_client.get_active_connections():
nm_profile = nm_ac.get_connection()
if nm_profile and nm_profile.get_id() == connection_name:
changed |= active_connection.deactivate_active_connection(
nm_ac, timeout, check_mode
)
if not changed:
logging.info("No active connection for {0}".format(connection_name))
return changed
def volatilize_connection_by_uuid(self, uuid, timeout, check_mode):
"""
Mark NM.RemoteConnection as volatile(delete on deactivation) via Update2,
if not supported, delete the profile.
Return True if changed.
"""
nm_client = client.get_client()
changed = False
for nm_profile in nm_client.get_connections():
if nm_profile and nm_profile.get_uuid() == uuid:
if hasattr(nm_profile, "update2"):
changed |= connection.volatilize_remote_connection(
nm_profile, timeout, check_mode
)
else:
changed |= connection.delete_remote_connection(
nm_profile, timeout, check_mode
)
if not changed:
logging.info("No connection with UUID {0} to volatilize".format(uuid))
return changed
def get_connections(self):
nm_client = client.get_client()
return nm_client.get_connections()

View File

@@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
# Write extra requirements for running molecule here:

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: MIT
# Write extra requirements for running pylint here:
mock
pytest

View File

@@ -0,0 +1,12 @@
# SPDX-License-Identifier: MIT
# Write extra requirements for running pytest here:
# If you need ansible then uncomment the following line:
#-ransible_pytest_extra_requirements.txt
# If you need mock then uncomment the following line:
mock ; python_version < "3.0"
# ansible and dependencies for all supported platforms
ansible ; python_version > "2.6"
ansible<2.7 ; python_version < "2.7"
idna<2.8 ; python_version < "2.7"
PyYAML<5.1 ; python_version < "2.7"

View File

@@ -0,0 +1,184 @@
#!/usr/bin/python3 -tt
# SPDX-License-Identifier: BSD-3-Clause
# Helper to print all options that the module in the network role accepts for
# profiles
from collections.abc import Mapping
from collections.abc import Sequence
from copy import deepcopy
from unittest import mock
import os
import sys
PRIORITIES = (
"name",
"type",
"interface_name",
"mac",
"state",
"persistent_state",
"controller",
"port_type",
"parent",
"ignore_errors",
"force_state_change",
"check_iface_exists",
"autoconnect",
"wait",
"zone",
"mtu",
"ip",
"ethernet",
"ethtool",
"bridge",
"bond",
"team",
"vlan",
"wireless",
"macvlan",
"infiniband",
)
import yaml
parentdir = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
with mock.patch.object(
sys,
"path",
[parentdir, os.path.join(parentdir, "module_utils/network_lsr")] + sys.path,
):
with mock.patch.dict(
"sys.modules",
{"ansible": mock.Mock(), "ansible.module_utils": __import__("module_utils")},
):
import argument_validator as av
COMMENT = "@@"
EMPTY = "/EMPTY/"
def parse_validator(validator):
default = validator.default_value
if isinstance(validator, av.ArgValidatorDict):
res = {}
for k, v in validator.nested.items():
if (
v.name
not in (
"infiniband_transport_mode",
"infiniband_p_key",
"vlan_id",
)
and not isinstance(v, av.ArgValidatorDeprecated)
):
name = k
if not validator.required:
pass
# name += " DICT optional"
res[name] = parse_validator(v)
elif isinstance(validator, av.ArgValidatorList):
res = [parse_validator(validator.nested)]
elif isinstance(validator, av.ArgValidatorNum):
minval = validator.val_min
maxval = validator.val_max
comment = f" {COMMENT}"
if not validator.required:
comment += " optional"
if minval is not None:
comment += " mininum=" + str(minval)
if maxval:
if maxval == 0xFFFFFFFF:
maxval = hex(maxval)
comment += " maximum=" + str(maxval)
if default is not None:
res = str(default)
elif minval is not None:
res = str(minval)
elif maxval is not None:
res = str(maxval)
else:
res = ""
res += comment
elif isinstance(validator, av.ArgValidatorIP):
res = f"{EMPTY} {COMMENT} IP Address"
elif isinstance(validator, av.ArgValidatorStr):
if default:
res = default
elif validator.enum_values:
res = "|".join(validator.enum_values)
else:
res = EMPTY
if not validator.required:
res += f" {COMMENT} optional"
# res += " " + str(validator.__class__)
elif isinstance(validator, av.ArgValidatorBool):
if default is not None:
res = "yes" if default else "no"
else:
res = "yes|no"
if not validator.required:
res += f" {COMMENT} optional"
else:
res = validator.name + f" {COMMENT} FIXME " + str(validator.__class__)
return res
def represent_dict(dumper, data):
"""
Represent dictionary with insert order
"""
value = []
for item_key, item_value in data.items():
node_key = dumper.represent_data(item_key)
node_value = dumper.represent_data(item_value)
value.append((node_key, node_value))
return yaml.nodes.MappingNode("tag:yaml.org,2002:map", value)
def priority_sorted(data):
if isinstance(data, Sequence) and not isinstance(data, str):
return [priority_sorted(item) for item in data]
if isinstance(data, Mapping):
sorted_data = {}
for key in sorted(data, key=prioritize):
sorted_data[key] = priority_sorted(data[key])
return sorted_data
return deepcopy(data)
def prioritize(key):
try:
priority = PRIORITIES.index(key)
except ValueError:
priority = len(PRIORITIES)
return (priority, key)
yaml.add_representer(dict, represent_dict)
sorted_data = priority_sorted([parse_validator(av.ArgValidator_DictConnection())])
yaml_example = (
yaml.dump(
sorted_data,
explicit_start=True,
default_flow_style=False,
width=100,
)
.replace(COMMENT, "#")
.replace(EMPTY, "")
)
# yaml_example = re.sub(r"# ([^:]*):", r": # \1", yaml_example)
print(yaml_example)

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
""" Check that there is a playbook to run all role tests with both providers
"""
# vim: fileencoding=utf8
import glob
import os
import sys
GET_NM_VERSION = """
- block:
- name: Install NetworkManager
package:
name: NetworkManager
state: present
- name: Get NetworkManager version
command: rpm -q --qf "%{version}" NetworkManager
args:
warn: false
register: NetworkManager_version
when: true
when:
- ansible_distribution_major_version != '6'
tags:
- always
"""
MINIMUM_NM_VERSION_CHECK = """
- NetworkManager_version.stdout is version({minimum_nm_version}, '>=')
"""
EXTRA_RUN_CONDITION_PREFIX = " - "
RUN_PLAYBOOK_WITH_NM = """# SPDX-License-Identifier: BSD-3-Clause
# This file was generated by ensure_provider_tests.py
---
# set network provider and gather facts
- hosts: all
name: Run playbook '{test_playbook}' with nm as provider
tasks:
- name: Set network provider to 'nm'
set_fact:
network_provider: nm
tags:
- always
{get_nm_version}
# The test requires or should run with NetworkManager, therefore it cannot run
# on RHEL/CentOS 6
- import_playbook: {test_playbook}
when:
- ansible_distribution_major_version != '6'
{minimum_nm_version_check}{extra_run_condition}"""
MINIMUM_VERSION = "minimum_version"
EXTRA_RUN_CONDITION = "extra_run_condition"
NM_ONLY_TESTS = {
"playbooks/tests_802_1x_updated.yml": {},
"playbooks/tests_802_1x.yml": {},
"playbooks/tests_eth_dns_support.yml": {},
"playbooks/tests_dummy.yml": {},
"playbooks/tests_ethtool_features.yml": {
MINIMUM_VERSION: "'1.20.0'",
"comment": "# NetworkManager 1.20.0 introduced ethtool settings support",
},
"playbooks/tests_ipv6_disabled.yml": {
EXTRA_RUN_CONDITION: "ansible_distribution_major_version == '8'",
},
"playbooks/tests_provider.yml": {
MINIMUM_VERSION: "'1.20.0'",
"comment": "# NetworKmanager 1.20.0 added support for forgetting profiles",
},
"playbooks/tests_ethtool_coalesce.yml": {
MINIMUM_VERSION: "'1.25.1'",
"comment": "# NetworkManager 1.25.1 introduced ethtool coalesce support",
},
"playbooks/tests_802_1x_updated.yml": {},
"playbooks/tests_802_1x.yml": {},
"playbooks/tests_reapply.yml": {},
# team interface is not supported on Fedora
"playbooks/tests_team.yml": {
EXTRA_RUN_CONDITION: "ansible_distribution != 'Fedora'",
},
"playbooks/tests_team_plugin_installation.yml": {},
# mac80211_hwsim (used for tests_wireless) only seems to be available
# and working on RHEL/CentOS 7
"playbooks/tests_wireless.yml": {
EXTRA_RUN_CONDITION: "ansible_distribution_major_version == '7'",
},
"playbooks/tests_wireless_plugin_installation.yml": {},
}
IGNORE = [
# checked by tests_regression_nm.yml
"playbooks/tests_checkpoint_cleanup.yml",
]
RUN_PLAYBOOK_WITH_INITSCRIPTS = """# SPDX-License-Identifier: BSD-3-Clause
# This file was generated by ensure_provider_tests.py
---
- hosts: all
name: Run playbook '{test_playbook}' with initscripts as provider
tasks:
- name: Set network provider to 'initscripts'
set_fact:
network_provider: initscripts
tags:
- always
- import_playbook: {test_playbook}
"""
def create_nm_playbook(test_playbook):
fileroot = os.path.splitext(os.path.basename(test_playbook))[0]
nm_testfile = fileroot + "_nm.yml"
minimum_nm_version = NM_ONLY_TESTS.get(test_playbook, {}).get(MINIMUM_VERSION)
extra_run_condition = NM_ONLY_TESTS.get(test_playbook, {}).get(
EXTRA_RUN_CONDITION, ""
)
if extra_run_condition:
extra_run_condition = "{}{}\n".format(
EXTRA_RUN_CONDITION_PREFIX, extra_run_condition
)
nm_version_check = ""
if minimum_nm_version:
nm_version_check = MINIMUM_NM_VERSION_CHECK.format(
minimum_nm_version=minimum_nm_version
)
nominal_nm_testfile_data = RUN_PLAYBOOK_WITH_NM.format(
test_playbook=test_playbook,
get_nm_version=minimum_nm_version and GET_NM_VERSION or "",
minimum_nm_version_check=nm_version_check,
extra_run_condition=extra_run_condition,
)
return nm_testfile, nominal_nm_testfile_data
def create_initscripts_playbook(test_playbook):
fileroot = os.path.splitext(os.path.basename(test_playbook))[0]
init_testfile = fileroot + "_initscripts.yml"
nominal_data = RUN_PLAYBOOK_WITH_INITSCRIPTS.format(test_playbook=test_playbook)
return init_testfile, nominal_data
def check_playbook(generate, testfile, test_playbook, nominal_data):
is_missing = False
returncode = None
if generate:
print(testfile)
with open(testfile, "w") as ofile:
ofile.write(nominal_data)
if not os.path.isfile(testfile) and not generate:
is_missing = True
else:
with open(testfile) as ifile:
testdata = ifile.read()
if testdata != nominal_data:
print(f"ERROR: Playbook does not match nominal value: {testfile}")
returncode = 1
return is_missing, returncode
def main():
testsfiles = glob.glob("playbooks/tests_*.yml")
missing = []
returncode = 0
# Generate files when specified
generate = bool(len(sys.argv) > 1 and sys.argv[1] == "generate")
if not testsfiles:
print("ERROR: No tests found")
returncode = 1
for test_playbook in testsfiles:
if test_playbook in IGNORE:
continue
nm_testfile, nominal_nm_testfile_data = create_nm_playbook(test_playbook)
is_missing, new_returncode = check_playbook(
generate=generate,
testfile=nm_testfile,
test_playbook=test_playbook,
nominal_data=nominal_nm_testfile_data,
)
if is_missing:
missing.append(test_playbook)
if new_returncode:
returncode = new_returncode
if test_playbook not in NM_ONLY_TESTS:
init_testfile, nominal_init_testfile_data = create_initscripts_playbook(
test_playbook
)
is_missing, new_returncode = check_playbook(
generate=generate,
testfile=init_testfile,
test_playbook=test_playbook,
nominal_data=nominal_init_testfile_data,
)
if is_missing:
missing.append(test_playbook)
if new_returncode:
returncode = new_returncode
if missing:
print("ERROR: No NM or initscripts tests found for:\n" + ", \n".join(missing))
print("Try to generate them with '{} generate'".format(sys.argv[0]))
returncode = 1
return returncode
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,B773C37C13C791B1B2F735A7D6D22F1D
KcpCACKK2i/zLDkH/e2bM/3hzyuC7UkSJ32Vn2xvH6ukKzOpt71PJjtzucY3TgB7
T8fYDJ0OGFfW/97M9OSjY10+wo/Vn+aTTCJWe2Y0+JeoV+bFJq33fuP0SlJI1PIU
CrxnWhFUM3iaDHjuJ32GaUCkLozKTRdb5KT0BttSdSudnT+9d6zHejCwvYEaGek0
C3fifoN2xC47P+63UF40KWMP0+j83ZRtHXUUgQ9E0Eqmbag6jTBh2TvV/PiaWlRv
YCVMapOBs0ktSPPJACygRJcR63MocS9of7aRaPMCDP7HpzrjzKnHqJ+bPteuaE4k
UmVOlrBsJb4g/zpfT4Ee2waT/mKEiRtNhf8a7DNkc34I50iMqhOojM1zRPtQugO6
5BGhFeciHCe7RzHvltWJRmLrl+H7Z8wvusxbSQRM5ZT18+wgBkgTb8dA3bmZS0Ws
JYcd9BN8zbsxETo/IFZ2gFOaVvOymVE5mscRR21RsiBi1vfqjl+pAt4ZrlGwVpxL
3z3yvT3lAx8Cgeg8dCxrDNb14Xwk+hkBblExLMXsUGCsRXJglk9QVPE0XjKD9XNa
mZnBHOpAsdPun58PRiaPpC+VgaFBhzPHTyBczCG1sjpkOiTJpGLpgveAq4wOXQGH
PMcux4ZDARYbJfGXANNqloIO3PHDPuhVmSAJZSMixDd4SLKjT6tALdqIv1BvOLl7
Ay0y3Vie4oGc4EWjHqQA+r+6CATHHXtIOvWLJQ4/KQa/R+pTp0qDtXdOeHaAZzhv
BpqvQUouKUyxXlGFZrGUq9l+sFtjLlcKP33Yb2WHg4ct0gAVDIA6SK4rNH6+h/NS
rFQNOvArTeZgLCaG6htJh68WLF8p6687s4bKNM8niZ5VcsFTvMYPbfF5WdE0l53s
fZpZBf1v03ZRJYg2V9a0sNPEysaIaTJzs5lFeya78iTF/Epo4GtTHv8sWebVwh/H
FYINLIcPzzxAvw7a+7ymIsYZphomuEoCCoX85DPPbXfZOb2Bdysfdr7uyRsB480E
or6+gQxZJWxcO5tMR7+G8EuUgnPMelVNczw3UJHM+sl4Kjh9q3hF4ppWFTIOaPQ/
BL3qPE/ZxSFC8UcG+QJEbNmPPQLXnpWPUZ3GmyH/+pPUZCkcWanpn0W3chGlJCsW
spkDMt/dpPtje1q7rfrWCVAYo4AeYzigSuxoyfpBfqcpD6wAssPQmWj4fFr91RW7
p/iLlACpevyecALrJpU65yGWDvGWlx+dEqvdz7FRUSTkVrted/W3pmro8eDAInWx
17VM0hHfNE00hwpGaga2CY8q3EC+3kApSE6d8dbBtSzBp4YZsGq+p+Xkj7mTc/rn
mXJazUSPjNhWooI+0pN2VxB3HRBloNjsQOLaWVcSiv6l3wKl70ZbBjPkikO05k+v
QXayu3i9RjXvhT974atOqoqCSigc8ROsCYGxgHjwVMU9Spc9i8y6PrgX9ID6yk9f
9YcJjmtEi6MYh0uXNkx2m6utMjgcuAqP8yfPqeBRK2SOoLuBM9JKP8tjwq4ZBawj
SuWe82zTRjR2oXMgNy6gBBDGky+W7kNaNw/KksZUxdiNhzeDRbDG8hMJI1HcY4xQ
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDizCCAnOgAwIBAgIUG1DftQ2xyrN+HE+KHLFmKHZnIkcwDQYJKoZIhvcNAQEL
BQAwVDELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE
CgwTRGVmYXVsdCBDb21wYW55IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAgFw0yMDA1
MDMwNTI2MTFaGA8yMjk0MDIxNTA1MjYxMVowVDELMAkGA1UEBhMCWFgxFTATBgNV
BAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEQ
MA4GA1UEAwwHVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AMGAmO9ugnI/jaw4qNTyh/O65BNEvzOIwLU0mo3wTOSiakoOuC0gqO4S+0FOmC6v
ceoArS+GllowzrgnnmM4EH9hqmiLeFKa4Z2graIm2W86ayN5k3psiMolONOZ8y0r
nAMj84FifDYIOHoYbKUeN5BDsotrHbrZ/PZhlZgN1ou3gapXqM12TkXdzaj//vRd
CORjwO1ubpzb17PFUNOLWaDf3ohfoMCG08UkGwIGK0mouJ1yflda27MCcLzmDxV8
4dfI//R/6WtN1hzWSW9ae99VwSjlACH2go/0fDD+K9jvKkEVRZAqBEnM3voQCOah
P9NMJ30R9Sh8B/D2KXGyIU0CAwEAAaNTMFEwHQYDVR0OBBYEFDUKdAwDiWpUpayU
mjiWEcMcXjQdMB8GA1UdIwQYMBaAFDUKdAwDiWpUpayUmjiWEcMcXjQdMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKEyNiDawDJeaauDUmHgdNlG
WuBlvn4Lph/+J27njmAoIbKv3aDw+kndxI02ryCZTJOm8a1NqHfkNct4ny+Cj4cz
rNoZIyMucVoKGgCMYb5zwYtW3W7RshUZoBdQDBLiIuktNsWTyqss3yVPPq8Q1JJY
89dtjCNydL6dunFSrGjVJ2K5HaTyidti2IN9g2Sbxmxgoz71ZP09xmBxaY+O738M
z5nRdrb2DX0flmv5pcqSzn7063t9FGKOp2bF9NTpcEWkultsCOvsVcsO4X/18L4J
3W8FVltyCvunv4GQecWqlNHTRT+QI2h48EVEzHQnOGEe9q1C8WVGeQ3cZXMei8k=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,31 @@
# password=test
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,C4A5E9A189773AB0F3CE3DCC98F208AE
LPNSExpEERS+/qHJxd8puT+EaZ/dZ20gkU/C2eaNNJerzr4moSXG4ioh5ggz4utQ
w57fD5OYqPiloNIawi/Ta5Opo3zU+iMZPVQALLbemXWXmNMxqxNCGdonc4enxMoN
auLxpdYPW+infFmf0UPwZjWkrLnK8XFapTGDaNesfgMNSRVSt+DQL3xeKUjcuXfh
rYvF26/Ls8NHB0tCU449vCa5ta1fHPT78B0cWgCmhcg/L8/0veBYfwxnyu6l3E6Q
RXWcyaJoihhCSg9kCZOqQFKDtz3B9G8/G8P5n5udN2TYUK0ieCktocOip0r/aUfk
Rz/NPjej18tuvA9e+uho2DuEj7OV1Rt0Fr6G2NySDYAIjlzM1+GoDdX3R8Rva2eX
SJYEjQvvLMAXU9wLEGd2u9jw3h8g2rNPF34Mo/fZsU6f83WceN7wzaDjKBM9TC/U
DjeUpJ2LHr3SduRoq5K7PqTG6LlRx4ZC06P8Gwu/cjlHqHuMlLE6wWPHowp9O08S
zMzJji6csSzZ5x5U41xiBJd19G0tbfjGBOvxhVLC3hmfqMtRwgeKSZMUz5f0iFvS
V4LE/ZNXWv5OybEzMyIiQBRB0G8mq5BkQ3rU9uTMO6Xc6mosQy0jiCsQLYaX2IoT
kyU6ZqPgAeBD3g5zCGudcF4qqY3pWRU6cijpivsuyX58YmulhQJsB2rnoImv8ZOR
4Uw+fvAx38v/dH/aAGKNdQV/4z+CXpAX4SdqYgBx9wXu6Wva31AVrbDrKnpSlWYF
M9gAHgpuhW9OH7du/y7sePU6k37fHtqDX0V5XoyeRxixR+KGb8k3tt0HFA1GExSu
XyXcOOfwec7xNQjZBM9jREI0yO1tCbHEeLsLpQnf31cpfSQumBZoiim6Vyk7vCN8
YBJ9qiVNrFiVogWl5hUrSS2MLQP1ZQBkedmOeKZpkZ26GW5yY0y27v2mHdhU2Dvd
otvLGiVKxSXlu+tqt1WkMvu6hcfrDZDCONW7emGW7xs2vdYdvADVlYs/Eb0WFXb1
tLkwg3v7I23LeFRrKX4Fm5/biG4GuR4sj9iPLayrKWhpujIVFJqHTI3YhjIU56Qp
uPuClnoFsKrWS9DXaziuuXmLZlXH3e5aOO+M2H3JmXTRCojyjKlIJiJJmHGrfwfe
oJkSF+ABs2zrpteXU+Cnfn8V01TrtxPYIBF3CbOMZEvwgjPLX0UNtnss0hXH4rJe
9yF/PiKWehUow8q4Gpwt2PnLkUWyL21GwCwXf5Cq3yRAKtyrJTlJsdYV1f3brzfb
JkBgKaFJ44Ee7D75PAio8g/BIDpvUdZVXwn3FizjfAU+HhXonPSYb2M34C6I/frk
mJPgZ5hbpt1SoCCER48+rQygiLdNQH6OsuhJeEElPFYwNo6i5jZsZ9iE0rmJxGgk
m7Mhi491NdK8L6Kh8kM2Dgupsfcstmx4+pI3gmgnsYZApmFoQlfcg4MhbWqxznv+
cPm1n2SZMoMLru44vbnjW+ZAggen5zNZOrsVt8UImSBVKfAIrgDUuYIv7uqUiKHI
yHmAkZDlqEbpkbUG9m60OeuEIgpN7MT3Kod387ZyOu9uaTZWdD18/N83E4eFecND
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuQipSk9+0rd/qBMDRiFzV8vDksaueVphejGEgiQhqtUDgjc/
ot/o7M8fFVC6wau2ixTnEMHuZXgoBOKATxX805FggEsLLL98OnN7AyTTKOtHVfIm
gK3fJ1Y9l95+2nuJWhmaan0vr3YMp6z3lSa+hlhhTYx/mIvTZho/K3+METg8DEfl
QUSkhAlrFSEahr2Pu/yETr8c+8vKTDnZDLvcFyyuDtAz+clEQUVndWJQpQpSVfR9
4xKuzaj10mUA9Utv4RkbNJ78/KgdTbaGIOVLUnYCJUg8d3/YV7aNCqraHBAZ9aoP
S4dl46KXC3qpaEBFfKaF+RcPSVUtc4eCQ74kKwIDAQABAoIBAGoArUN2IVjEaSy3
n7OIrFSK1oL6sa+x+JARWDFaU7NTj0wFLL65ee5Yhh0m/6a+IbiyA+IUx+d3m62Y
uRsVpJ7r9RXqZ/99v8SYrctSSGpzx41USXyEn4ggnu6nN5MhHMHyUwVYrH3fqkZR
EBFxfcrnTO8pY1vYFwayWKgpzOt7ip30JF1E7RH0IWfA2koJ+hZgSumPmF31btBK
eqDaQ168u0at6I7nYvRIWVT68D2k+PMb/c/rlOUYSyy+VfCgnShWD+m1hlyaDF1c
cbVvOhsul3rFeEqbToGN/6yyDDcyolTvYxMm3vb6jmoExZyRsShv0XyhokSuCN9P
v5SeNpkCgYEA7OpIlsZUoTXm2ffCQiZd8gRtKk0O3dzmWTkcNEgj2uUNH6ANNy3W
gLojKeF2EyC3appRWLVRYN/m6r/Qj+rztZfW3Jw1UJQV+tLEOBzk3yBnRdh1aRgW
8YTH1+HJqlJ/2iKJRKRhseM5AHiTslp7ude6cWQxO52pJ6Rbp1z3fBUCgYEAx/B4
LreIDJYDnYSyL/CvVkHEn1hCYX0oBpefzV6ofYDqv0OLe8BWOBsShQ3Crh0FuQTa
xV2xc+OzDewlu2OwNm4/X0qjXvoWkEMLBXKEHjPyxnbHLCYaaA/9ENmVIkc8aZWE
p7KcCYGlfiHpbdYWAD8KYdv5CsFHFbwhPwrD7z8CgYAEtsSq+1dDvebR/3QGDO1h
m2TwqofZMkQDEnfVMnpEKLqSHoUky+ywswNwGeRXjRcZL+jecv0jiFD36skjk/E1
c8f6q8ED0W5+hyMQWsLTDboAUcZESQ5rz9CKIxv4H5wbowRIMV0gRP0lXUDTE6nS
kNBM4Ul5fjGXcFXChr8F4QKBgGSmAeoKi9tCHTnLVePaNnmmi/Nm+6uV1HNVGqXI
k+rx3bpAp1O5o+2Ee1MtdSYvB/V2oyadnrnnEvjcOrZVXZxY7V/r88fY/0jJ5x9r
4WRO5FTR8DuiRsLB4bP8xB1IXPoNwYSl3fTPJd8T9S1MizC+i1xt3rVyTHV9igLx
SWcDAoGBAMoynJvQUOssWwFTtNQK0ptz95rrTkO2bri+8MJfSh8tessekwPHVe6M
SBofFhDiesrHBHczJ61qDnb3GemA0kEbo023mxNo0HPam+OFgX5mrihizBZnRZjh
aecVouDd0uwacsB76fwP6Fl5GhkFvOSBKr2IKNJjUMXyvW8/XGZE
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrDCCApSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwVDELMAkGA1UEBhMCWFgx
FTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55
IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAgFw0yMDA1MDMwODUxMTdaGA8yMjk0MDIx
NTA4NTExN1owXzELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc
MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEbMBkGA1UEAwwSY2xpZW50LmV4
YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuQipSk9+
0rd/qBMDRiFzV8vDksaueVphejGEgiQhqtUDgjc/ot/o7M8fFVC6wau2ixTnEMHu
ZXgoBOKATxX805FggEsLLL98OnN7AyTTKOtHVfImgK3fJ1Y9l95+2nuJWhmaan0v
r3YMp6z3lSa+hlhhTYx/mIvTZho/K3+METg8DEflQUSkhAlrFSEahr2Pu/yETr8c
+8vKTDnZDLvcFyyuDtAz+clEQUVndWJQpQpSVfR94xKuzaj10mUA9Utv4RkbNJ78
/KgdTbaGIOVLUnYCJUg8d3/YV7aNCqraHBAZ9aoPS4dl46KXC3qpaEBFfKaF+RcP
SVUtc4eCQ74kKwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1P
cGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUoUCV4T3pFwaQ
HYSlCr8Iqdd+/TcwHwYDVR0jBBgwFoAUNQp0DAOJalSlrJSaOJYRwxxeNB0wDQYJ
KoZIhvcNAQELBQADggEBALXhDSFirybmhZXcHuSqXn0tLp6mZintW+91B81bDUtO
FuCrWqXwV0iensm94mOeykGIR/r0Y0Y4uqOHpIznY+q5NIek0qIdirbdr5mCXK5y
fxXVIMM14GMTyIR9A4+IZaRkFbcrVnBhOdUpTQjp88jlzDr5jdyjTEnOZyOJH9kL
Qpd417iB4X5TxuQ2xe5EgHOCb8OfxO0a2BzlwtfUQAkz2v+h0RlVBwQFcE2NCJ3z
hvF3AWGl+5pkfWpY6d+1EPI3+82C6uRf8be/WKHPKu3i0irrVtZdMsKNkRiD5UUK
S4Y0WnoVu/DWSR8h9iPGSFKMkUcjFI8hgc4YQ6G4Odc=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAjbYPkANn2XGqDGCzse9wAfM0I5WJpp+Xl+iNJFmaKXBguo0BPYQt
hZOpJbKL3aNaFsRxhdAJ8UXzBP6oIzCejcGti+jw+xtVk8ietWEK6e91yi+Ak2g2
/Xtt9hoYQkeoe5hkcv35NcJ0xdQwlSvMbY/j8HtKamx/A3zu+YPQAe/3AOe3L+JT
iEL5Gw00NPVnyEWKX4fVchAbMUkRsQKeXtsyOyDc4/RccjfLa1toyj8PRommK5UH
dkSqi04FTOUIx6aTwt21EehJuggLVDShoQdxGV+FzXmdtelLmerGMtVPBbf8DSkN
MKMBEg4d28DzjXPAWUHMD+JGPzAlvf87EwIBAg==
-----END DH PARAMETERS-----

View File

@@ -0,0 +1,31 @@
# password=test
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,ED349A8B098E2D1DB70C30F77EF599AB
j1rzje2sWFk3B9kD6eE7WrqVDynFEJ3t3kdOv0iUvH5Ybll1C7Qx3EFEdoM4z2OV
E6q3nr2DOvpMPox1DvBdIipWOQWJxkZyBHqNn4v4GR4c0uxLswsk7XSBQLUclRsn
QBGO6x8pcEA9u/O3PSrTt+pVozWrXWmR2UHNM//9WUsRpWF4Lv0EINzsfwmD7aJQ
nRcSfXsCggXP6wnJX5dgo5PlRm6R+bodgzePr0QRlh8TT6wnixZfWalYM5iUKlEF
GcE+VejZuBL69byl2AcRt8I5tQ+UZxmzhKPSsYN0NKD8vbcVVnp2sre/rbdTzWz5
laF386g1M8QBimDE/V3Bw5b9Bg1ZP3arlpugVXGVNA+HFti8PVdkaMqLgkFIC2Xu
OwmNKffAPIItuB8leg5A76oLoIlllRqjWO9M/O+MqAlrJ96xLRiUeGkez4Pp7eFV
30YrlOXyzwZKfXoOPIfE5Mbz4CPqR67XuqW8jOryIGOryMB17b0+vdRpDY0wxk8/
lGmc5rglDxLFA8dNemAHDednasCuVlrbsQsZRnPkKavXiSu7QCbvm1frAXZfnyRp
TpPmE6L4+nEy8PQnK/IxOCqRcy6e1SPezRpajRjB5ooDT8hDmDkG47NdnrB+kOKL
5LIpATLSGS9IVk0RW/M8EqJP1kRh2JOCQT3V+gUN0ttz8bjZpivKnp76/ztg0lo0
oC2lhuXV5HOYHw1z5jDazsYpQDYoHgYWXnzPJJp6Ecn+nkjZMKQjDV9ZqE1miPrZ
E4V0ULNmWaAQHvwc98yR97ui1YHmw5XVMoeDhy3fhB6IOyaGGdEj9o2iQr8kp9GC
dxBKK/xMOU6kwDF9Nsfh46veRGTbhAJdGeWqdxscdCupkO8KRtZqzL454+9GnYfe
n1f7wxJh7aTLNjF2an5Qa9v7uU6D58+9blxG7ls5qGt4xjBNAXCc8bPpmLqeCW4G
Xz8iwxECvwWIQ+SjUcXuP8+/NO58B14kDNP03+1gA7AHIesa2CTvHLCyMPaN2oGK
3R4LNxQQDNygEzRj8vHjURU1FNRJ4RjCi7SbqoOsl31Hvef6j0lcW0Sz4UICcCJI
p4NPnApoaHewL4exvlJ80qPbFscuVevXBlUC2LdxXS+9E+c0NaLauEeNYCUoaBDi
HIpbxRKXmqLc4LAKYVuEcIBFhdXp3UC9niVd7Nrguu0lUJXC78OzpltxWrqX/u4E
O2aCNK0Yg9U+rxm6wyccqEyptIS2GRCIpUGD/LVF3mOC16NB/JeYGrOWvDptdCeg
9pJrakJjE1Fm3pg4Xc74bT6IDj0EKwKSvZhtlcsM9JaXWChe/ZrDPPI/NP6MuyW4
jcqpa9HPBBSyaxKsEPXFJhdhrz8VfsU2e5VvcALaJaAOpHwZgaNUpvpsY4LPW9mi
lHsecEBiq6re0r7TAgBE1AnlaI4ho0fKSgSub3NWUZlEaBK3X2n/Li6op6LIsvM5
iySYaAluQy4dANww0KhQHMIh0jbuZGzmG2Hxk/poorYRf60YJlbTnHVD/FKUdFX+
rUow0iy8Ez1uF272u5orYW2tBbkhSaieKOT8f4HFCxUsgITbd8Lf/XJ6l6Qns6SK
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrDCCApSgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwVDELMAkGA1UEBhMCWFgx
FTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55
IEx0ZDEQMA4GA1UEAwwHVGVzdCBDQTAgFw0yMDA1MDMwODUxMzBaGA8yMjk0MDIx
NTA4NTEzMFowXzELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc
MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEbMBkGA1UEAwwSc2VydmVyLmV4
YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcC44Amd
KQBDwR67aMTPqmNu6HfjadZqsD2xZj5XMVdn4karqsVYIbKMOq+SRzgm5aZ/kzQI
CpXJMXfj16cID6BCxNecfJVOfvPyI0kCUbMf1YZiRG2FmB2VsG8AVDGWmn4a7SmX
yaCA0ac8dkipnlCF2nddLhcBak/Ls+hjRYN7VSLLvxO8KT42ivhuP9YgGY1K5Yta
e90H4HBiKxbnkwOUxi9wobERSXSLgb4e+uX8WRrqxIIYmHF+Gzv5kilRFrPwKBmo
3idVPrqjschZe0o8m/nbNo3SzWGI9fdXn0+KgZoQdG3ZixX6uOhrCqJ3iJmnHkp4
aXKL5Y7JmX/FFQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1P
cGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUx7TXCxUioob7
5r/1kMypCYy9Mj0wHwYDVR0jBBgwFoAUNQp0DAOJalSlrJSaOJYRwxxeNB0wDQYJ
KoZIhvcNAQELBQADggEBAKtTPl4WJuxfMeut+aEw7vVRU+z5A7D35nlZPQI5nBTt
ybgqMNIjdcYT/JwT2GhbzcObc3STNEo582clVN9gTpK7mYKzBBf69nTsWeZzPuNt
JQbVbK4RHwFvyosJcw6NfzxE9OxeXhTcKQDQSGKP338sAWoapEZlXNrYOIJac6HX
Xo3dQqx/8BdO9hSv1u0/zClnL5lbk1RBylS24wIe8wLoiy4ftLjL4aOYOlonj7HU
hknTY6L30oOpG5VtH8SEv3xveH/5GNKwfoGltTzemCgVfb9IhyVTLB3tIv8OW6k1
y3+YEzVniVB4gtJ5UniLN1V4lBf6t7MGn0ybAEbOxPI=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,65 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
if [ -n "${DEBUG}" ]
then
set -x
fi
set -e
if [ "$#" -lt 2 ]
then
echo "USAGE: ${0} host playbook"
echo "Get coverage info from host for playbook"
exit 1
fi
host="${1}"
shift
playbook="${1}"
coverage_data="remote-coveragedata-${host}-${playbook%.yml}"
coverage="/root/.local/bin/coverage"
echo "Getting coverage for ${playbook} on ${host}" >&2
call_ansible() {
local module="${1}"
shift
local args="${1}"
shift
ansible -m "${module}" -i "${host}", -a "${args}" all "${@}"
}
remote_coverage_dir="$(mktemp -d /tmp/remote_coverage-XXXXXX)"
trap "rm -rf '${remote_coverage_dir}'" EXIT
ansible-playbook -i "${host}", get_coverage.yml -e "test_playbook=${playbook} destdir=${remote_coverage_dir}"
#COVERAGE_FILE=remote-coverage coverage combine remote-coverage/tests_*/*/root/.coverage
./merge_coverage.sh coverage "${coverage_data}"-tmp $(find "${remote_coverage_dir}" -type f | tr , _)
cat > tmp_merge_coveragerc <<EOF
[paths]
source =
.
EOF
# example path with Ansible 2.9.6:
# /tmp/ansible_network_connections_payload_psugdf6r/ansible_network_connections_payload.zip/ansible/modules/network_connections.py
# /tmp/ansible_network_connections_payload_psugdf6r/ansible_network_connections_payload.zip/ansible/module_utils/network_lsr/__init__.py
# /tmp/ansible_network_connections_payload_psugdf6r/ansible_network_connections_payload.zip/ansible/module_utils/network_lsr/argument_validator.py
# /tmp/ansible_network_connections_payload_psugdf6r/ansible_network_connections_payload.zip/ansible/module_utils/network_lsr/utils.py
# /tmp/ansible_network_connections_payload_psugdf6r/ansible_network_connections_payload.zip/ansible/module_utils/network_lsr/nm_provider.py
for file in $(echo 'SELECT path FROM file;' | sqlite3 "${coverage_data}"-tmp | sed s,/module.*.py,, | sort -u)
do
echo " ${file}" >> tmp_merge_coveragerc
done
COVERAGE_FILE="${coverage_data}" coverage combine --rcfile tmp_merge_coveragerc "${coverage_data}"-tmp
test -n "${DEBUG}" && cat tmp_merge_coveragerc
rm tmp_merge_coveragerc
COVERAGE_FILE="${coverage_data}" coverage report ||:
COVERAGE_FILE="${coverage_data}" coverage html --directory "htmlcov-${coverage_data}" ||:
echo "Coverage collected in: ${coverage_data}"

View File

@@ -0,0 +1,82 @@
# SPDX-License-Identifier: BSD-3-Clause
---
# This expects the variable test_playbook to be set from the outside
- name: Prepare for coverage extraction
hosts: all
tasks:
# Use set_fact to set variables to make them available in all plays
# 'vars:' Would only set variables for the current play
- name: set facts
set_fact:
coverage_module: network_connections
coverage: /root/.local/bin/coverage
destdir: "remote_coverage/{{ test_playbook }}"
# This uses variables from the other set_fact task, therefore it needs to
# be its own task
- name: set more facts
set_fact:
coverage_file:
# yamllint disable-line rule:line-length
ansible-coverage-{{ coverage_module }}-{{ test_playbook|replace('.yml', '') }}
- name: debug info
debug:
msg:
# yamllint disable-line rule:line-length
Getting coverage for '{{ coverage_module }}' with '{{ test_playbook }}'
# combine data in case old data is left there
- command: "{{ coverage }} combine"
environment:
COVERAGE_FILE: "{{ coverage_file }}"
ignore_errors: yes
- name: remove old data
file:
state: absent
path: "{{ coverage_file }}"
- name: find coverage files to delete
find:
path: "{{ ansible_env.HOME }}"
patterns: ".coverage.*"
hidden: yes
register: files_to_delete
- name: remove old data
file:
path: "{{ item.path }}"
state: absent
with_items: "{{ files_to_delete.files }}"
- name: copy coveragerc
copy:
content: "[run]\ndisable_warnings = no-data-collected\n"
dest: .coveragerc
- name: install latest pip
pip:
name: coverage
extra_args: --user --upgrade
- import_playbook: "{{ test_playbook }}"
vars:
ansible_python_interpreter:
# yamllint disable-line rule:line-length
"{{ coverage }} run -p --include /*/modules/network_connections.py,/*/module_utils/network_lsr/*"
- name: Gather coverage data
hosts: all
tasks:
- shell: "{{ coverage }} combine .coverage.*"
environment:
COVERAGE_FILE: "{{ coverage_file }}"
- name: Get coverage data
hosts: all
tasks:
- fetch:
src: "{{ coverage_file }}"
dest: "{{ destdir }}"
flat: no

View File

@@ -0,0 +1,34 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
set -e
coverage_data=total-coveragedata
testhost="${1}"
if [ "$#" -lt 1 ]
then
echo "USAGE: ${0} host"
echo "Get local and all remote coverage data for host"
exit 1
fi
rm -f remote-coveragedata* "${coveragedata}"
# collect pytest coverage
tox -e py26,py27,py36,py37 -- --cov-append
for test_playbook in tests_*.yml
do
./get_coverage.sh "${testhost}" "${test_playbook}"
done
./merge_coverage.sh coverage "total-remote-coveragedata" remote-coveragedata-*
./covstats .coverage remote-coveragedata-* "total-remote-coveragedata"
./merge_coverage.sh coverage "${coverage_data}" .coverage remote-coveragedata-*
echo "Total coverage:"
COVERAGE_FILE="${coverage_data}" coverage report ||:
COVERAGE_FILE="${coverage_data}" coverage html --directory "htmlcov-${coverage_data}" ||:
echo "Open HTML report with:"
echo "xdg-open htmlcov-${coverage_data}/index.html"

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*
# SPDX-License-Identifier: BSD-3-Clause
def pytest_addoption(parser):
parser.addoption(
"--provider", action="store", default="nm", help="Network provider"
)

View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*
# SPDX-License-Identifier: BSD-3-Clause
import logging
import os
import subprocess
import pytest
try:
from unittest import mock
except ImportError:
import mock
parent_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
with mock.patch.dict(
"sys.modules",
{
"ansible.module_utils.basic": mock.Mock(),
},
):
import network_connections as nc
class PytestRunEnvironment(nc.RunEnvironment):
def log(self, connections, idx, severity, msg, **kwargs):
if severity == nc.LogLevel.ERROR:
logging.error("Error: {}".format(connections[idx]))
raise RuntimeError(msg)
else:
logging.debug("Log: {}".format(connections[idx]))
def run_command(self, argv, encoding=None):
command = subprocess.Popen(
argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
return_code = command.wait()
out, err = command.communicate()
return return_code, out.decode("utf-8"), err.decode("utf-8")
def _check_mode_changed(self, *args, **kwargs):
pass
def _configure_network(connections, provider):
cmd = nc.Cmd.create(
provider,
run_env=PytestRunEnvironment(),
connections_unvalidated=connections,
connection_validator=nc.ArgValidator_ListConnections(),
)
cmd.run()
@pytest.fixture(scope="session")
def provider(request):
return request.config.getoption("--provider")
@pytest.fixture
def testnic1():
veth_name = "testeth"
try:
subprocess.call(
[
"ip",
"link",
"add",
veth_name,
"type",
"veth",
"peer",
"name",
veth_name + "peer",
],
close_fds=True,
)
yield veth_name
finally:
subprocess.call(["ip", "link", "delete", veth_name])
def _get_ip_addresses(interface):
ip_address = subprocess.check_output(["ip", "address", "show", interface])
return ip_address.decode("UTF-8")
@pytest.fixture
def network_lsr_nm_mock():
with mock.patch.dict(
"sys.modules",
{
"ansible.module_utils.basic": mock.Mock(),
},
):
yield
def test_static_ip_with_ethernet(testnic1, provider, network_lsr_nm_mock):
ip_address = "192.0.2.127/24"
connections = [
{
"name": testnic1,
"type": "ethernet",
"state": "up",
"ip": {"address": [ip_address]},
}
]
_configure_network(connections, provider)
assert ip_address in _get_ip_addresses(testnic1)
if provider == "initscripts":
assert os.path.exists("/etc/sysconfig/network-scripts/ifcfg-" + testnic1)
else:
subprocess.check_call(["nmcli", "connection", "show", testnic1])

View File

@@ -0,0 +1,35 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
if [ -n "${DEBUG}" ]
then
set -x
fi
set -e
if [ "$#" -lt 3 ]
then
echo "USAGE: ${0} path_to_coverage_binary output_file input_files..."
echo "Merges all input_files into output file without removing input_files"
exit 1
fi
# path to coverage binary
coverage="${1}"
shift
# read by coverage binary
export COVERAGE_FILE="${1}"
shift
tempdir="$(mktemp -d /tmp/coverage_merge-XXXXXX)"
trap "rm -rf '${tempdir}'" EXIT
cp --backup=numbered -- "${@}" "${tempdir}"
# FIXME: Would not work if coverage files are not hidden but they are by
# default
shopt -s dotglob
"${coverage}" combine "${tempdir}/"*
echo "Merged data into ${COVERAGE_FILE}"
./covstats "${COVERAGE_FILE}"

View File

@@ -0,0 +1 @@
../module_utils/

View File

@@ -0,0 +1 @@
../library/

View File

@@ -0,0 +1,10 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Set {{ profile }} down
hosts: all
vars:
network_connections:
- name: "{{ profile }}"
state: down
roles:
- linux-system-roles.network

View File

@@ -0,0 +1 @@
../files

Some files were not shown because too many files have changed in this diff Show More