Browse Source

Merge branch 'wip-libvirtd' into 'master'

implement the libvirt infrastructure driver

Closes #268

See merge request main/infrastructure!302
keep-around/c737dd06ec7808b0e277c8dca725578533d92a9b
Loïc Dachary 7 months ago
parent
commit
c737dd06ec
  1. 2
      .dockerignore
  2. 2
      .gitignore
  3. 1
      Pipfile
  4. 139
      Pipfile.lock
  5. 7
      docs/community/contribute.rst
  6. 19
      enough/common/__init__.py
  7. 9
      enough/common/data/base.dockerfile
  8. 17
      enough/common/dotenough.py
  9. 26
      enough/common/host.py
  10. 300
      enough/common/libvirt.py
  11. 15
      enough/common/openstack.py
  12. 59
      enough/common/service.py
  13. 15
      enough/common/ssh.py
  14. 2
      inventory/group_vars/all/infrastructure.yml
  15. 5
      inventory/group_vars/all/libvirt.yml
  16. 4
      inventory/group_vars/all/network.yml
  17. 9
      local-libvirt-playbook.yml
  18. 1
      playbooks/bind/bind-client-dhcp-playbook.yml
  19. 87
      playbooks/bind/roles/dhclient/tasks/main.yml
  20. 3
      playbooks/bind/roles/dhclient/templates/resolv.conf.j2
  21. 19
      playbooks/bind/tests/test_external_bind.py
  22. 13
      playbooks/bind/tests/test_resolvconf_stability.py
  23. 7
      playbooks/certificate/roles/certificate/tasks/ca.yml
  24. 6
      playbooks/chat/chat-playbook.yml
  25. 69
      playbooks/conftest.py
  26. 6
      playbooks/gitlab/gitlab-ci-playbook.yml
  27. 1
      playbooks/gitlab/roles/debops.ansible_plugins
  28. 1
      playbooks/gitlab/roles/debops.libvirtd
  29. 1
      playbooks/gitlab/roles/debops.libvirtd_qemu
  30. 1
      playbooks/gitlab/roles/debops.secret
  31. 2
      playbooks/gitlab/roles/gitlab-ci/tasks/gitlab-ci.yml
  32. 4
      playbooks/icinga/tests/test_icinga_checks.py
  33. 11
      playbooks/icinga/tests/test_icingaweb.py
  34. 11
      playbooks/icinga/tests/test_letsencrypt.py
  35. 94
      playbooks/infrastructure/network.sh
  36. 1
      playbooks/infrastructure/roles/docker/tasks/main.yml
  37. 10
      playbooks/infrastructure/roles/libvirt/tasks/main.yml
  38. 103
      playbooks/infrastructure/template-host.yaml
  39. 21
      playbooks/postfix/tests/test_postfix.py
  40. 10
      requirements-dev.txt
  41. 13
      requirements.txt
  42. 168
      tests/__init__.py
  43. 65
      tests/conftest.py
  44. 4
      tests/enough/common/conftest.py
  45. 4
      tests/enough/common/test_common_host.py
  46. 25
      tests/enough/common/test_init.py
  47. 52
      tests/enough/common/test_libvirt.py
  48. 5
      tests/entrypoint.sh
  49. 16
      tests/icinga_helper.py
  50. 8
      tests/infrastructure.py
  51. 11
      tests/named.conf.local
  52. 6
      tests/named.conf.options
  53. 37
      tests/run-tests.sh
  54. 11
      tests/tox.dockerfile

2
.dockerignore

@ -7,3 +7,5 @@ venv
**/*.pyo
inventory/group_vars/all/clouds.yml
inventory/group_vars/all/domain.yml
**/*.qcow2
**/*.img

2
.gitignore

@ -1,3 +1,5 @@
**/*.qcow2
**/*.img
*~
*.retry
__pycache__

1
Pipfile

@ -38,3 +38,4 @@ python-openstackclient = "*"
python-heatclient = "*"
sh = "==1.12.14"
shade = "==1.30.0"
libvirt-python = "*"

139
Pipfile.lock

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "a38dc59d609be1e6fca99941984abb928923fd7891fffd830d6ca30d85758e2a"
"sha256": "8cc6418cf556c6d7df2d2f67166c0158565dc32c57d146569e631b73306e0e10"
},
"pipfile-spec": 6,
"requires": {},
@ -16,10 +16,10 @@
"default": {
"ansible": {
"hashes": [
"sha256:736a19fa6d608b4df2d6b48d31fec057b3f95abf62b7fda69ffa4a743e2f55b6"
"sha256:53a39a4df7010b279c5c7895558f8943b267e88d81488375aabf08e7b0633148"
],
"index": "pypi",
"version": "==2.9.15"
"version": "==2.9.16"
},
"appdirs": {
"hashes": [
@ -123,10 +123,11 @@
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==3.0.4"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"cliff": {
"hashes": [
@ -237,11 +238,11 @@
"ssh"
],
"hashes": [
"sha256:317e95a48c32de8c1aac92a48066a5b73e218ed096e03758bcdd799a7130a1a1",
"sha256:cffc771d4ea1389fc66bc95cb72d304aa41d1a1563482a9a000fba3a84ed5071"
"sha256:0604a74719d5d2de438753934b755bfcda6f62f49b8e4b30969a4b0a2a8a1220",
"sha256:e455fa49aabd4f22da9f4e1c1f9d16308286adc60abaf64bf3e1feafaed81d06"
],
"index": "pypi",
"version": "==4.4.0"
"version": "==4.4.1"
},
"docker-compose": {
"hashes": [
@ -340,6 +341,13 @@
"markers": "python_version >= '3.6'",
"version": "==4.3.0"
},
"libvirt-python": {
"hashes": [
"sha256:47a8e90d9f49bc0296d2817f6009e18dbb69844ce10b81c2a2672bccd6f49fd5"
],
"index": "pypi",
"version": "==6.10.0"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
@ -381,46 +389,36 @@
},
"msgpack": {
"hashes": [
"sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903",
"sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d",
"sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a",
"sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820",
"sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b",
"sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61",
"sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1",
"sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b",
"sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f",
"sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4",
"sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f",
"sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608",
"sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98",
"sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e",
"sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34",
"sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a",
"sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd",
"sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975",
"sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd",
"sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b",
"sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f",
"sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484",
"sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315",
"sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05",
"sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060",
"sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a",
"sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697",
"sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979",
"sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6",
"sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d",
"sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e",
"sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc",
"sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed",
"sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb",
"sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650",
"sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8",
"sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261",
"sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab"
],
"version": "==1.0.1"
"sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9",
"sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841",
"sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439",
"sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694",
"sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a",
"sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f",
"sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e",
"sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1",
"sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c",
"sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b",
"sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759",
"sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326",
"sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc",
"sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192",
"sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83",
"sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06",
"sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e",
"sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9",
"sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33",
"sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54",
"sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f",
"sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887",
"sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009",
"sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2",
"sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c",
"sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87",
"sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984",
"sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"
],
"version": "==1.0.2"
},
"munch": {
"hashes": [
@ -681,10 +679,10 @@
},
"pytz": {
"hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
],
"version": "==2020.4"
"version": "==2020.5"
},
"pyyaml": {
"hashes": [
@ -706,11 +704,11 @@
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.0"
"version": "==2.25.1"
},
"requests-oauthlib": {
"hashes": [
@ -925,10 +923,11 @@
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==3.0.4"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
"hashes": [
@ -1161,11 +1160,11 @@
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.9.0"
"version": "==1.10.0"
},
"pycodestyle": {
"hashes": [
@ -1231,10 +1230,10 @@
},
"pytz": {
"hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
],
"version": "==2020.4"
"version": "==2020.5"
},
"readme-renderer": {
"hashes": [
@ -1245,11 +1244,11 @@
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.0"
"version": "==2.25.1"
},
"requests-mock": {
"hashes": [
@ -1331,11 +1330,11 @@
},
"tqdm": {
"hashes": [
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
"sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"
"sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416",
"sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.54.1"
"version": "==4.55.0"
},
"twine": {
"hashes": [

7
docs/community/contribute.rst

@ -56,6 +56,13 @@ Debian GNU/Linux buster. The following volumes are bind-mounted:
The working directory, in the container, is the root of the
`infrastructure` repository.
Installing libvirt
~~~~~~~~~~~~~~~~~~
Manually run commands similar to
`playbooks/gitlab/roles/gitlab-ci/tasks/gitlab-ci.yml` (it could be a
playbook running on localhost with sudo sudo ?)
Running tests that do not require OpenStack
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

19
enough/common/__init__.py

@ -6,10 +6,12 @@ import shutil
import textwrap
import yaml
from enough.common.dotenough import DotEnoughOpenStack, DotEnough, Hosts
from enough.common.dotenough import DotEnoughOpenStack, DotEnoughLibvirt, Hosts
from enough.common.host import host_factory
from enough.common.service import service_factory
from enough.common.openstack import OpenStack, Heat
from enough.common.libvirt import Libvirt
from enough.common.ssh import SSH
from enough.common import retry
from enough.common import ansible_utils
@ -37,20 +39,25 @@ class Enough(object):
self.args['cloud'] = cloud
self.dotenough.populate_provider(self.ansible, cloud)
else:
self.dotenough = DotEnough(self.config_dir, self.args['domain'])
self.dotenough = DotEnoughLibvirt(self.config_dir, self.args['domain'])
self.hosts = Hosts(self.config_dir)
self.host = host_factory(self.config_dir, self.share_dir, **self.args)
self.service = service_factory(self.config_dir, self.share_dir, **self.args)
if self.args.get('driver') == 'openstack':
self.openstack = OpenStack(self.config_dir, **self.args)
self.heat = Heat(self.config_dir, **self.args)
elif self.args.get('driver') == 'libvirt':
self.libvirt = Libvirt(self.config_dir, self.share_dir, **self.args)
self.playbook = ansible_utils.Playbook(self.config_dir, self.share_dir,
self.args.get('inventory'))
def create_missings(self, hosts):
public_key = f'{self.config_dir}/infrastructure_key.pub'
r = self.heat.create_missings(hosts, public_key)
self.update_internal_subnet_dns()
if self.args.get('driver') == 'openstack':
public_key = f'{self.config_dir}/infrastructure_key.pub'
r = self.heat.create_missings(hosts, public_key)
self.update_internal_subnet_dns()
elif self.args.get('driver') == 'libvirt':
r = self.libvirt.create_or_update(Hosts(self.config_dir).missings(hosts))
return r
def update_internal_subnet_dns(self):
@ -281,7 +288,7 @@ class Enough(object):
raise Enough.VolumeResizeNoSize(f'no size found for {volume} in the openstack_volumes '
f'variable ({volumes}) for {host}')
r = self.openstack.volume_resize(host, volume, int(volume_size))
self.openstack.wait_for_ssh(d['ansible_host'], d['ansible_port'])
SSH.wait_for_ssh(d['ansible_host'], d['ansible_port'])
return r
def info(self):

9
enough/common/data/base.dockerfile

@ -3,6 +3,10 @@ ARG USER_NAME
ENV USER_NAME ${USER_NAME:-root}
ARG DOCKER_GID
ENV DOCKER_GID ${DOCKER_GID:-999}
ARG LIBVIRT_GID
ENV LIBVIRT_GID ${LIBVIRT_GID:-136}
ARG KVM_GID
ENV KVM_GID ${KVM_GID:-108}
ARG USER_ID
ENV USER_ID ${USER_ID:-0}
@ -14,7 +18,10 @@ RUN groupadd --gid $DOCKER_GID docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
RUN curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
RUN if test $USER_NAME != root ; then useradd --no-create-home --home-dir /tmp --uid $USER_ID --groups $DOCKER_GID $USER_NAME && echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers ; fi
RUN groupadd --gid $LIBVIRT_GID libvirt
RUN groupadd --gid $KVM_GID kvm
RUN if test $USER_NAME != root ; then useradd --no-create-home --home-dir /tmp --uid $USER_ID --groups $DOCKER_GID,$LIBVIRT_GID,$KVM_GID $USER_NAME && echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers ; fi
ENV REQUESTS_CA_BUNDLE /etc/ssl/certs
WORKDIR /opt

17
enough/common/dotenough.py

@ -137,6 +137,16 @@ class DotEnough(object):
"""))
class DotEnoughLibvirt(DotEnough):
def __init__(self, config_dir, domain):
super().__init__(config_dir, domain)
def ensure(self):
super().ensure()
self.populate_config('ownca')
class DotEnoughOpenStackUnknownProvider(Exception):
pass
@ -183,10 +193,3 @@ class DotEnoughOpenStack(DotEnough):
openstack_provider: {provider}
"""))
return f
class DotEnoughDocker(DotEnough):
def ensure(self):
super().ensure()
self.populate_config('ownca')

26
enough/common/host.py

@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
from enough import settings
from enough.common import openstack
from enough.common import libvirt
from enough.common import dotenough
@ -51,6 +52,31 @@ class HostOpenStack(Host):
s.delete_wait()
class HostLibvirt(Host):
def __init__(self, config_dir, share_dir, **kwargs):
super().__init__(config_dir, share_dir, **kwargs)
self.args = kwargs
self.dotenough = dotenough.DotEnoughLibvirt(config_dir, self.args['domain'])
self.dotenough.ensure()
def create_or_update(self):
name = self.args['name']
dotenough.Hosts(self.config_dir).ensure(name)
lv = libvirt.Libvirt(self.config_dir, self.share_dir, **self.args)
return lv.create_or_update([name])[name]
def delete(self):
self.delete_hosts(self.args['name'])
def delete_hosts(self, names):
lv = libvirt.Libvirt(self.config_dir, self.share_dir, **self.args)
for name in names:
lv.delete(name)
def host_factory(config_dir=settings.CONFIG_DIR, share_dir=settings.SHARE_DIR, **kwargs):
if kwargs['driver'] == 'openstack':
return HostOpenStack(config_dir, share_dir, **kwargs)
elif kwargs['driver'] == 'libvirt':
return HostLibvirt(config_dir, share_dir, **kwargs)

300
enough/common/libvirt.py

@ -0,0 +1,300 @@
import libvirt
import logging
import os
import sh
import textwrap
from enough.common.ssh import SSH
from enough.common import ansible_utils
from enough.common.dotenough import Hosts
from enough.common import retry
log = logging.getLogger(__name__)
class Libvirt(object):
BIND_MAC = '52:54:00:00:00:02'
NETWORK = {
'external': {
'prefix': '10.23.10',
'name': 'enough-ext',
},
'internal': {
'prefix': '10.23.90',
'name': 'enough-int',
},
}
def __init__(self, config_dir, share_dir, **kwargs):
self.args = kwargs
self.config_dir = config_dir
self.share_dir = share_dir
self.lv = libvirt.open('qemu:///system')
self.ansible = ansible_utils.Ansible(self.config_dir, self.share_dir,
self.args.get('inventory'))
self.domain = kwargs.get('domain', 'enough.community')
self.images_dir = f'/var/lib/libvirt/images/enough/{self.domain}'
self.network_definitions = None
def host_image_name(self, name):
return f'{self.images_dir}/{name}.qcow2'
def public_key(self):
return f'{self.config_dir}/infrastructure_key.pub'
def sysprep(self, name, definition):
fqdn = f'{name}.{self.domain}'
sh.virt_sysprep(
'-a', self.host_image_name(name),
'--enable', 'customize',
'--no-network',
'--hostname', fqdn,
'--run-command', f'sed -i -e "s/^127.0.1.1.*/127.0.1.1 {fqdn}/" /etc/hosts',
'--ssh-inject', f'debian:file:{self.public_key()}',
'--copy-in', f'{self.share_dir}/playbooks/infrastructure/network.sh:/root',
'--firstboot-command', ('env '
f'PORT={definition["port"]} '
f'ROUTED={definition["network_interface_routed"]} '
f'NOT_ROUTED={definition["network_interface_not_routed"]} '
f'UNCONFIGURED={definition["network_interface_unconfigured"]} '
'bash -x /root/network.sh'),
)
def get(self, name):
try:
return self.lv.lookupByName(name)
except libvirt.libvirtError:
return None
def _create_or_update(self, definition):
name = definition['name']
if self.get(name) is not None:
return None
log.info(f"{name}: building image")
self.image_builder()
sh.cp('--sparse=always', self.image_name(), self.host_image_name(name))
log.info(f"{name}: preparing image")
self.sysprep(name, definition)
log.info(f"{name}: creating host")
sh.env('HOME=/tmp',
'virt-install',
'--connect', 'qemu:///system',
'--network', f"network={definition['network-external']}{definition['mac']}",
'--network', f"network={definition['network-internal']}",
'--boot', 'hd',
'--name', name,
'--memory', definition['ram'],
'--vcpus', '1',
'--cpu', 'host',
'--disk', f'path={self.host_image_name(name)},bus=virtio,format=qcow2',
'--os-type=linux',
'--os-variant=debian10',
'--graphics', 'none',
'--noautoconsole')
log.info(f"{name}: waiting for ipv4 to be allocated")
info = {
'ipv4': self.get_ipv4(name),
'port': definition['port'],
}
log.info(f"{name}: waiting for {info['ipv4']}:{info['port']} to come up")
SSH.wait_for_ssh(info['ipv4'], info['port'])
Hosts(self.config_dir).create_or_update(
definition['name'], info['ipv4'], info['port'])
log.info(f"{name}: host is ready")
return info
@retry.retry((libvirt.libvirtError, AssertionError), 8)
def get_ipv4(self, name):
dom = self.lv.lookupByName(name)
ifaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
assert ifaces, f'interfaceAddresses returned {ifaces}'
for (name, val) in ifaces.items():
addrs = val['addrs']
assert len(addrs) == 1, f"{addrs} expected len is 1"
addr = val['addrs'][0]
assert addr['type'] == libvirt.VIR_IP_ADDR_TYPE_IPV4
return addr['addr']
def create_or_update(self, names):
self.networks_create()
definitions = self.get_definitions()
r = {}
for name in names:
r[name] = self._create_or_update(
self.get_definition(name, definitions[name]))
return r
def get_definitions(self):
return self.ansible.get_hostvars(variable=None)
def get_definition(self, name, definition):
r = {}
if name == 'bind-host':
r['mac'] = f',mac={Libvirt.BIND_MAC}'
else:
r['mac'] = ''
for network in ('external', 'internal'):
r[f'network-{network}'] = definition.get(
f'libvirt_network_{network}_name',
Libvirt.NETWORK[network]['name'],
)
r.update({
'name': name,
'port': definition.get('ansible_port', '22'),
'ram': definition.get('libvirt_ram', '2048'),
# 'disk': definition.get('libvirt_disk', '20G'),
'network_interface_unconfigured': definition.get('network_interface_unconfigured'),
'network_interface_routed': definition.get('network_interface_routed'),
'network_interface_not_routed': definition.get('network_interface_not_routed'),
})
return r
@staticmethod
def _image_name():
return 'debian-10.qcow2'
def image_name(self):
return f'{self.images_dir}/{self._image_name()}'
@staticmethod
def _image_builder(image):
if os.path.exists(image):
return False
sh.virt_builder(
'debian-10',
'--no-cache',
'--output', image,
'--format', 'qcow2',
'--size', '20G',
'--install', 'sudo',
'--root-password', 'disabled',
'--run-command', 'dpkg-reconfigure --frontend=noninteractive openssh-server',
'--run-command', ('useradd -s /bin/bash -m debian || true ; '
'echo "debian ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-debian'),
)
sh.chmod('0660', image)
sh.chgrp('libvirt', image)
return True
def image_dir_ensure(self):
if not os.path.exists(self.images_dir):
os.makedirs(self.images_dir)
sh.chmod('0771', self.images_dir)
sh.chgrp('libvirt', self.images_dir)
def image_builder(self):
self.image_dir_ensure()
return self._image_builder(self.image_name())
def networks_definitions_get(self):
if self.network_definitions:
return self.network_definitions
variables = ('libvirt_network_external_prefix',
'libvirt_network_internal_prefix',
'libvirt_network_external_name',
'libvirt_network_internal_name')
variables = "{%s}" % ', '.join(f'"{x}": {x}' for x in variables)
network_vars = self.ansible.get_global_variable(variables)
d = {}
for network in ('external', 'internal'):
vars = {}
for var in ('prefix', 'name'):
vars[var] = network_vars.get(
f'libvirt_network_{network}_{var}',
Libvirt.NETWORK[network][var],
)
d[network] = vars
self.network_definitions = d
return self.network_definitions
def networks_create(self):
r = []
d = self.networks_definitions_get()
for network in ('external', 'internal'):
vars = d[network]
r.append(self.network_create(vars['name'], vars['prefix']))
if network == 'external':
self.network_host_set(vars['name'],
'bind-host',
Libvirt.BIND_MAC,
f'{vars["prefix"]}.2')
return r
def networks_destroy(self):
for (_, network) in self.networks_definitions_get().items():
self.network_destroy(network['name'])
def network_host_definition(self, host, mac, ip):
return f"<host mac='{mac}' name='{host}' ip='{ip}'/>"
def network_host_set(self, name, host, mac, ip):
network = self.lv.networkLookupByName(name)
xml = self.network_host_definition(host, mac, ip)
if xml in network.XMLDesc():
return False
network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST,
libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST,
-1,
xml,
libvirt.VIR_NETWORK_UPDATE_AFFECT_CURRENT)
return True
def network_host_unset(self, name, host, mac, ip):
network = self.lv.networkLookupByName(name)
xml = self.network_host_definition(host, mac, ip)
if xml not in network.XMLDesc():
return False
network.update(libvirt.VIR_NETWORK_UPDATE_COMMAND_DELETE,
libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST,
-1,
xml,
libvirt.VIR_NETWORK_UPDATE_AFFECT_CURRENT)
return True
def network_create(self, name, prefix):
if name not in self.lv.listNetworks():
network = textwrap.dedent(f"""
<network>
<name>{name}</name>
<forward mode='nat'/>
<bridge name='virbr{name}' stp='on' delay='0'/>
<ip address='{prefix}.1' netmask='255.255.255.0'>
<dhcp>
<range start='{prefix}.100' end='{prefix}.254'/>
</dhcp>
</ip>
</network>
""")
network = self.lv.networkDefineXML(network)
network.create()
network.autostart()
else:
network = self.lv.networkLookupByName(name)
return network
def network_destroy(self, name):
if name in self.lv.listNetworks():
network = self.lv.networkLookupByName(name)
network.destroy()
network.undefine()
return True
else:
return False
def delete(self, name):
domain = self.get(name)
if domain:
domain.destroy()
domain.undefine()
Hosts(self.config_dir).delete(name)
return domain is not None
def destroy_everything(self, prefix):
for network in self.lv.listNetworks():
if prefix in network:
self.network_destroy(network)
for domain in self.lv.listAllDomains():
if prefix in domain.name():
self.delete(domain.name())

15
enough/common/openstack.py

@ -7,7 +7,6 @@ import os
import re
import requests
import sh
import socket
import textwrap
import time
import yaml
@ -16,6 +15,7 @@ from enough import settings
from enough.common import ansible_utils
from enough.common.retry import retry
from enough.common.dotenough import Hosts
from enough.common.ssh import SSH
log = logging.getLogger(__name__)
@ -38,16 +38,6 @@ class OpenStackBase(object):
_env={'OS_CLIENT_CONFIG_FILE': self.config_file},
)
@staticmethod
@retry((socket.timeout, ConnectionRefusedError, OSError), 9)
def wait_for_ssh(ip, port):
log.info('Check if SSH is available on %s:%s', ip, port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((ip, int(port)))
line = s.makefile("rb").readline()
assert line.startswith(b'SSH-')
class Stack(OpenStackBase):
@ -113,7 +103,7 @@ class Stack(OpenStackBase):
'port': self.definition['port'],
}
if not self.internal_only() or self.args.get('route_to_internal', True):
self.wait_for_ssh(info['ipv4'], info['port'])
SSH.wait_for_ssh(info['ipv4'], info['port'])
Hosts(self.config_dir).create_or_update(
self.definition['name'], info['ipv4'], info['port'])
return info
@ -206,6 +196,7 @@ class Heat(OpenStackBase):
'flavor': hv['openstack_flavor'],
'image': hv['openstack_image'],
'network': hv.get('openstack_network'),
'network_internal_only': hv.get('network_internal_only'),
'network_interface_unconfigured': hv.get('network_interface_unconfigured'),
'network_interface_routed': hv.get('network_interface_routed'),
'network_interface_not_routed': hv.get('network_interface_not_routed'),

59
enough/common/service.py

@ -6,6 +6,7 @@ import yaml
from enough import settings
from enough.common import openstack
from enough.common import libvirt
from enough.common import dotenough
from enough.common import ansible_utils
@ -107,6 +108,25 @@ class Service(object):
services.append(service)
return services
def create_or_update(self):
self.ensure_non_empty_service_group()
hosts = self.service2hosts[self.args['name']]
self.create_missings(hosts)
self.maybe_delegate_dns()
playbook = ansible_utils.Playbook(self.config_dir, self.share_dir)
if os.path.isabs(self.args["playbook"]):
playbook_file = self.args["playbook"]
else:
playbook_file = f'{self.config_dir}/{self.args["playbook"]}'
if not os.path.exists(playbook_file):
playbook_file = f'{self.share_dir}/{self.args["playbook"]}'
playbook.run([
f'--private-key={self.dotenough.private_key()}',
'--limit', ','.join(hosts + ['localhost']),
playbook_file,
])
return {'fqdn': f'{self.args["name"]}.{self.args["domain"]}'}
def info(self, hostvars, service, show_passwords):
domain = self.args['domain']
fields = []
@ -241,27 +261,32 @@ class ServiceOpenStack(Service):
r.raise_for_status()
return True
def create_or_update(self):
self.ensure_non_empty_service_group()
hosts = self.service2hosts[self.args['name']]
def create_missings(self, hosts):
h = openstack.Heat(self.config_dir, cloud=self.args['cloud'])
h.create_missings(hosts, self.dotenough.public_key())
self.maybe_delegate_dns()
playbook = ansible_utils.Playbook(self.config_dir, self.share_dir)
if os.path.isabs(self.args["playbook"]):
playbook_file = self.args["playbook"]
else:
playbook_file = f'{self.config_dir}/{self.args["playbook"]}'
if not os.path.exists(playbook_file):
playbook_file = f'{self.share_dir}/{self.args["playbook"]}'
playbook.run([
f'--private-key={self.dotenough.private_key()}',
'--limit', ','.join(hosts + ['localhost']),
playbook_file,
])
return {'fqdn': f'{self.args["name"]}.{self.args["domain"]}'}
class ServiceLibvirt(Service):
class PingException(Exception):
pass
def __init__(self, config_dir, share_dir, **kwargs):
super().__init__(config_dir, share_dir, **kwargs)
self.args = kwargs
self.dotenough = dotenough.DotEnoughLibvirt(config_dir, self.args['domain'])
self.dotenough.ensure()
def create_missings(self, hosts):
lv = libvirt.Libvirt(self.config_dir, self.share_dir, **self.args)
lv.create_or_update(dotenough.Hosts(self.config_dir).missings(hosts))
def maybe_delegate_dns(self):
pass
def service_factory(config_dir=settings.CONFIG_DIR, share_dir=settings.SHARE_DIR, **kwargs):
if kwargs['driver'] == 'openstack':
return ServiceOpenStack(config_dir, share_dir, **kwargs)
elif kwargs['driver'] == 'libvirt':
return ServiceLibvirt(config_dir, share_dir, **kwargs)

15
enough/common/ssh.py

@ -1,7 +1,12 @@
import logging
import socket
import subprocess
import sys
from enough.common import dotenough
from enough.common.retry import retry
log = logging.getLogger(__name__)
class SSH(object):
@ -29,3 +34,13 @@ class SSH(object):
else:
kwargs = dict(capture_output=True)
return subprocess.run(ssh + args, **kwargs)
@staticmethod
@retry((socket.timeout, ConnectionRefusedError, OSError), 8)
def wait_for_ssh(ip, port):
log.info('Check if SSH is available on %s:%s', ip, port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((ip, int(port)))
line = s.makefile("rb").readline()
assert line.startswith(b'SSH-')

2
inventory/group_vars/all/infrastructure.yml

@ -2,5 +2,5 @@
#
# Server provisioning infrastructure
#
# infrastructure_driver: docker
# infrastructure_driver: libvirt
infrastructure_driver: openstack

5
inventory/group_vars/all/libvirt.yml

@ -0,0 +1,5 @@
---
libvirt_network_external_name: enough-ext
libvirt_network_external_prefix: 10.23.10
libvirt_network_internal_name: enough-int
libvirt_network_internal_prefix: 10.23.90

4
inventory/group_vars/all/network.yml

@ -15,8 +15,8 @@
# the default route is network_secondary_interface
#
network_internal_only: false
network_primary_interface: eth0
network_secondary_interface: eth1
network_primary_interface: "{{ infrastructure_driver == 'openstack' | ternary('eth0', 'enp1s0') }}"
network_secondary_interface: "{{ infrastructure_driver == 'openstack' | ternary('eth1', 'enp2s0') }}"
#
# Only one of the two interfaces is routed
#

9
local-libvirt-playbook.yml

@ -0,0 +1,9 @@
---
- name: setup libvirt locally
hosts: localhost
gather_facts: false
become: true
roles:
- role: libvirt

1
playbooks/bind/bind-client-dhcp-playbook.yml

@ -16,6 +16,5 @@
# the name resolution will fail and sudo will timeout
#
- name: set hostname
when: infrastructure_driver == 'openstack'
hostname:
name: '{{ inventory_hostname }}.{{ domain }}'

87
playbooks/bind/roles/dhclient/tasks/main.yml

@ -1,55 +1,40 @@
---
- when: infrastructure_driver == 'openstack'
block:
#
# The following is only necessary for backward compatibility. Once all Enough
# instances have a separate dhclient_routers.conf for the secondary
# interface, it can be removed, as well as the template.
#
- name: setup dhclient.conf
template:
src: dhclient.conf.j2
dest: /etc/dhcp/dhclient.conf
register: dhclient
#
# The following is only necessary for backward compatibility. Once all Enough
# instances have a separate dhclient_routers.conf for the secondary
# interface, it can be removed, as well as the template.
#
- name: setup dhclient.conf
template:
src: dhclient.conf.j2
dest: /etc/dhcp/dhclient.conf
register: dhclient
#
# The following is necessary if the host was not created by Enough
# because it is created by playbooks/infrastructure/template-host.yaml
#
- name: touch /etc/dhcp/dhclient_routers.conf
file:
path: /etc/dhcp/dhclient_routers.conf
state: touch
mode: 0644
#
# The following is necessary if the host was not created by Enough
# because it is created by playbooks/infrastructure/template-host.yaml
#
- name: touch /etc/dhcp/dhclient_routers.conf
file:
path: /etc/dhcp/dhclient_routers.conf
state: touch
mode: 0644
- name: setup dhclient_routers.conf
blockinfile:
block: |
supersede domain-name "{{ dns_domain }}";
supersede domain-search "{{ dns_domain }}";
supersede domain-name-servers {{ bind_server_ip_for_clients }};
path: /etc/dhcp/dhclient_routers.conf
marker: "# {mark} Enough"
register: dhclient_routers
- name: setup dhclient_routers.conf
blockinfile:
block: |
supersede domain-name "{{ dns_domain }}";
supersede domain-search "{{ dns_domain }}";
supersede domain-name-servers {{ bind_server_ip_for_clients }};
path: /etc/dhcp/dhclient_routers.conf
marker: "# {mark} Enough"
register: dhclient_routers
- name: restart all interfaces to reload dhclient.conf and update /etc/resolv.conf
shell: |
set -ex
ifdown {{ network_primary_interface }}
ifup {{ network_primary_interface }}
ifdown {{ network_secondary_interface }} || true
ifup {{ network_secondary_interface }} || true
when: dhclient is changed or dhclient_routers is changed
- when: infrastructure_driver == 'docker'
block:
- name: setup resolv.conf
template:
src: resolv.conf.j2
# copying directly to /etc/resolv.conf won't work because it is bind mounted by docker
dest: /tmp/resolv.conf
- name: copy to resolv.conf
shell: |
cat /tmp/resolv.conf > /etc/resolv.conf
- name: restart all interfaces to reload dhclient.conf and update /etc/resolv.conf
shell: |
set -ex
ifdown {{ network_primary_interface }}
ifup {{ network_primary_interface }}
ifdown {{ network_secondary_interface }} || true
ifup {{ network_secondary_interface }} || true
when: dhclient is changed or dhclient_routers is changed

3
playbooks/bind/roles/dhclient/templates/resolv.conf.j2

@ -1,3 +0,0 @@
search {{ dns_domain }}
nameserver {{ bind_server_ip_for_clients }}
nameserver 127.0.0.11

19
playbooks/bind/tests/test_external_bind.py

@ -1,9 +1,12 @@
from tests.infrastructure import get_driver
import pytest
testinfra_hosts = ['ansible://external-host']
def test_bind_external(host):
def test_external_bind(request, host):
if request.session.infrastructure.driver == 'libvirt':
pytest.skip("libvirt has no public IP therefore no external host")
bind_host = host.get_host('ansible://bind-host',
ansible_inventory=host.backend.ansible_inventory)
domain = bind_host.run("hostname -d").stdout.strip()
@ -12,18 +15,16 @@ def test_bind_external(host):
with host.sudo():
host.run("apt-get install -y dnsutils")
infrastructure = get_driver()
if infrastructure == 'openstack':
cmd = host.run(f"dig ns1.{domain}")
print(cmd.stdout)
print(cmd.stderr)
assert 0 == cmd.rc
cmd = host.run(f"dig ns1.{domain}")
print(cmd.stdout)
print(cmd.stderr)
assert 0 == cmd.rc
cmd = host.run(f"dig axfr {domain} @{address}")
print(cmd.stdout)
print(cmd.stderr)
assert 0 == cmd.rc
# recursion is prohibited
cmd = host.run(f"dig fsf.org @{address} | grep -q '^fsf.org'")
print(cmd.stdout)

13
playbooks/bind/tests/test_resolvconf_stability.py

@ -1,15 +1,14 @@
import pytest
from tests.infrastructure import get_driver
testinfra_hosts = ['ansible://bind-host', 'ansible://bind-client-host']
def test_resolvconf(host):
if get_driver() == 'docker':
pytest.skip("docker does not do dhcp")
resolvconf_before = host.run("cat /etc/resolv.conf").stdout.strip()
with host.sudo():
cmd = host.run("ifdown eth0 ; ifup eth0 ; ifdown eth1 ; ifup eth1")
assert 0 == cmd.rc
host.run("""
for i in eth0 eth1 enp1s0 enp2s0 ; do
ifdown $i
ifup $i
done
""")
resolvconf_after = host.run("cat /etc/resolv.conf").stdout.strip()
assert resolvconf_before == resolvconf_after

7
playbooks/certificate/roles/certificate/tasks/ca.yml

@ -49,6 +49,12 @@
selfsigned_not_after: "{{ certificate_year_2 }}"
selfsigned_digest: sha256
- name: ln -sf ca.crt `openssl x509 -hash -noout -in ca.crt`.0
shell: |
ln -sf ca.crt `openssl x509 -hash -noout -in ca.crt`.0
args:
chdir: "{{ certificate_local_directory }}/{{ certificate_year_0 }}"
- name: cd {{ certificate_local_directory }} ; ln -sf {{ certificate_year_0 }}/* .
shell: |
ln -sf {{ certificate_year_0 }}/{{ item }} .
@ -57,6 +63,7 @@
with_items:
- ca_privatekey.key
- ca.crt
- "*.0"
- name: mkdir -p /usr/local/share/ca-certificates/infrastructure
when: (certificate_authority == "ownca") or (certificate_authority == "letsencrypt_staging")

6
playbooks/chat/chat-playbook.yml

@ -5,10 +5,8 @@
pre_tasks:
- when: infrastructure_driver == 'openstack'
block:
- include_role:
name: ansible-role-docker
- include_role:
name: ansible-role-docker
roles:
- role: docker

69
playbooks/conftest.py

@ -1,10 +1,8 @@
import os
import pytest
import shutil
import yaml
from enough.common import Enough
from tests import make_config_dir, prepare_config_dir
from tests import InfrastructureOpenStack, InfrastructureLibvirt
def pytest_addoption(parser):
@ -23,6 +21,12 @@ def pytest_addoption(parser):
action="store_true",
help="Do not run the destroy step"
)
parser.addoption(
"--enough-driver",
default='openstack',
choices=['libvirt', 'openstack'],
help="Infrastructure driver libvirt or openstack"
)
def pytest_configure(config):
@ -36,27 +40,41 @@ def get_hosts(session, enough):
def pytest_sessionstart(session):
if session.config.getoption("--enough-no-create"):
return
service_directory = session.config.getoption("--enough-service")
enough_dot_dir = session.config.cache.makedir('dotenough')
driver = session.config.getoption("--enough-driver")
if driver == 'openstack':
session.infrastructure = InfrastructureOpenStack()
elif driver == 'libvirt':
session.infrastructure = InfrastructureLibvirt()
session.infrastructure.prefix_set(service_directory)
session.infrastructure.domain_set()
session.infrastructure.config_dir_set(enough_dot_dir)
if not session.config.getoption("--enough-no-destroy"):
enough_destroy(session)
hosts = session.config.getoption("--enough-hosts").split(',')
session.infrastructure.destroy(hosts)
session.infrastructure.clobber()
service_directory = session.config.getoption("--enough-service")
domain = f'{service_directory}.test'
enough_dot_dir = session.config.cache.makedir('dotenough')
config_dir = prepare_config_dir(domain, enough_dot_dir)
if session.config.getoption("--enough-no-create"):
return
session.infrastructure.prepare_config_dir(enough_dot_dir)
e = Enough(config_dir, '.',
domain=domain,
driver='openstack',
e = Enough(session.infrastructure.config_dir, '.',
domain=session.infrastructure.domain,
driver=driver,
inventory=[f'playbooks/{service_directory}/inventory'],
route_to_internal=False)
(vpn_host, hosts) = get_hosts(session, e)
r = e.create_missings(hosts)
with open(f'{e.config_dir}/inventory/group_vars/all/domain.yml', 'r') as f:
data = yaml.safe_load(f)
if len(r) > 0 or data.get('domain') == domain:
if (driver == 'openstack' and
(len(r) > 0 or
data.get('domain') == session.infrastructure.domain)):
e.heat.create_test_subdomain('enough.community')
if vpn_host in hosts:
if not e.vpn_has_credentials():
@ -86,24 +104,11 @@ def pytest_runtest_setup(item):
def pytest_sessionfinish(session, exitstatus):
if not session.config.getoption("--enough-no-destroy"):
enough_destroy(session)
def enough_destroy(session):
service_directory = session.config.getoption("--enough-service")
domain = f'{service_directory}.test'
enough_dot_dir = session.config.cache.makedir('dotenough')
config_dir = make_config_dir(domain, enough_dot_dir)
all_dir = f'{config_dir}/inventory/group_vars/all'
if not os.path.exists(all_dir):
os.makedirs(all_dir)
shutil.copyfile('inventory/group_vars/all/clouds.yml', f'{all_dir}/clouds.yml')
e = Enough(config_dir, '.',
domain=domain,
driver='openstack',
inventory=[f'playbooks/{service_directory}/inventory'])
e.destroy()
if session.config.getoption("--enough-no-destroy"):
return
hosts = session.config.getoption("--enough-hosts").split(',')
session.infrastructure.destroy(hosts)
session.infrastructure.clobber()
def pytest_unconfigure(config):

6
playbooks/gitlab/gitlab-ci-playbook.yml

@ -3,6 +3,9 @@
hosts: runner-host
become: true
environment: '{{ inventory__environment | d({})
| combine(inventory__group_environment | d({}))
| combine(inventory__host_environment | d({})) }}'
roles:
- role: ansible-role-docker
docker_install_compose: false
@ -10,4 +13,7 @@
- role: certificate
certificate_create: false
- role: debops.libvirtd
- role: debops.libvirtd_qemu
- role: gitlab-ci

1
playbooks/gitlab/roles/debops.ansible_plugins

@ -0,0 +1 @@
../../debops/ansible/roles/debops.ansible_plugins

1
playbooks/gitlab/roles/debops.libvirtd

@ -0,0 +1 @@
../../debops/ansible/roles/debops.libvirtd

1
playbooks/gitlab/roles/debops.libvirtd_qemu

@ -0,0 +1 @@
../../debops/ansible/roles/debops.libvirtd_qemu

1
playbooks/gitlab/roles/debops.secret

@ -0,0 +1 @@
../../debops/ansible/roles/debops.secret

2
playbooks/gitlab/roles/gitlab-ci/tasks/gitlab-ci.yml

@ -57,6 +57,8 @@
--url https://gitlab.{{ domain }} \
--executor docker \
--docker-image debian:buster \
--docker-devices /dev/kvm \
--docker-volumes /run/libvirt/libvirt-sock:/run/libvirt/libvirt-sock \
--docker-volumes /srv:/srv \
--docker-volumes /etc/ssl/certs:/etc/ssl/certs:ro \
--docker-volumes /usr/local/share/ca-certificates/infrastructure:/usr/local/share/ca-certificates/infrastructure:ro \

4
playbooks/icinga/tests/test_icinga_checks.py

@ -47,8 +47,8 @@ class TestChecks(IcingaHelper):
assert self.is_service_ok('icinga-host!memory')
assert self.is_service_ok('website-host!memory')
def test_grafana(self):
(s, a) = self.get_web_session()
def test_grafana(self, request):
(s, a) = self.get_web_session(request.session.infrastructure.certs())
@retry.retry(AssertionError, tries=8)
def has_image():

11
playbooks/icinga/tests/test_icingaweb.py

@ -1,6 +1,4 @@
import pytest
import requests
from tests.infrastructure import get_driver
import yaml
testinfra_hosts = ['ansible://bind-host']
@ -12,15 +10,14 @@ def get_address(inventory):
open(vars_dir + '/domain.yml'))['domain']
def test_icingaweb2_login_screen(host, pytestconfig):
if get_driver() == 'docker':
pytest.skip("no letsencrypt when running docker")
def test_icingaweb2_login_screen(request, host, pytestconfig):
certs = request.session.infrastructure.certs()
address = get_address(pytestconfig.getoption("--ansible-inventory"))
proto_srv = f"https://{address}"
s = requests.Session()
r = s.get(proto_srv+'/icingaweb2/authentication/login', timeout=20, verify='certs')
r = s.get(proto_srv+'/icingaweb2/authentication/login', timeout=20, verify=certs)
cookies = dict(r.cookies)
r = s.get(proto_srv+'/icingaweb2/authentication/login?_checkCookie=1',
cookies=cookies, timeout=5, verify='certs')
cookies=cookies, timeout=5, verify=certs)
r.raise_for_status()
assert 'Icinga Web 2 Login' in r.text

11
playbooks/icinga/tests/test_letsencrypt.py

@ -1,6 +1,4 @@
import pytest
import requests
from tests.infrastructure import get_driver
import yaml
testinfra_hosts = ['ansible://icinga-host']
@ -12,18 +10,17 @@ def get_address(inventory):
open(vars_dir + '/domain.yml'))['domain']
def test_icingaweb2_login_screen(host, pytestconfig):
if get_driver() == 'docker':
pytest.skip("no letsencrypt when running docker")
def test_icingaweb2_login_screen(request, host, pytestconfig):
certs = request.session.infrastructure.certs()
address = get_address(pytestconfig.getoption("--ansible-inventory"))
s = requests.Session()
r = s.get(f'http://{address}/icingaweb2/authentication/login',
timeout=5, allow_redirects=False)
assert r.status_code == 301
r = s.get(f'https://{address}/icingaweb2/authentication/login',
timeout=5, verify='certs')
timeout=5, verify=certs)
cookies = dict(r.cookies)
r = s.get(f'https://{address}/icingaweb2/authentication/login?_checkCookie=1',
cookies=cookies, timeout=5, verify='certs')
cookies=cookies, timeout=5, verify=certs)
r.raise_for_status()
assert 'Icinga Web 2 Login' in r.text

94
playbooks/infrastructure/network.sh

@ -0,0 +1,94 @@
#!/bin/bash
#
# Prefer IPv4 because IPv6 is not supported
#
sed -i -e 's|^#precedence ::ffff:0:0/96 100|precedence ::ffff:0:0/96 100|' /etc/gai.conf
echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
if [ "$PORT" -ne "22" ]; then
# Reload SSH
sed -i -e '/^#Port/s/^.*$/Port '$PORT'/' /etc/ssh/sshd_config
systemctl reload ssh
fi