Commit 34d9043d authored by Loïc Dachary's avatar Loïc Dachary
Browse files

Merge branch 'wip-libvirt' into 'master'

prepare a libvirt hypervisor

See merge request !461
parents a4b3f001 2d256eb2
Pipeline #1885 passed with stage
in 17 minutes and 21 seconds
......@@ -25,6 +25,24 @@ It can be set in `~/.enough/example.com/inventory/host_vars/bind-host/zone.yml`
@ 1800 IN MX 10 spool.mail.gandi.net.
Delegation
----------
To enable DNS delegation for ``example.com``, the following records
need to be added:
.. code::
example.com. IN NS bind.example.com.
bind.example.com. IN A 51.178.60.121
The DNS delegation for a subdomain follows the same logic:
.. code::
sub.example.com. IN NS bind.sub.example.com.
bind.sub.example.com. IN A 51.178.60.121
Host Resolver
-------------
......
Using Enough
============
Requirements
------------
Technical
~~~~~~~~~
* The ``clouds.yml`` credentials for either OVH or Fuga.
* The current code base requires:
* The `Orchestration API available <https://wiki.openstack.org/wiki/Heat>`__:
`Heat`, at least ``heat_template_version``: ``2016-10-14``/``newton``.
* A public IPv4 per virtual machine (not a *floating IP* but a *direct IP*).
IPv6 isn't supported yet.
* In order to deploy every available services: 15 virtual machines.
* The flavors must be provided with an attached root disk by default (not an
explicit block storage) most of the virtual machines use 2Go RAM, some
hosts/tests require 4Go or 8Go RAM.
* A `Debian GNU/Linux <https://www.debian.org/>`_ stable image.
Knowledge
~~~~~~~~~
---------
Enough is based on `Ansible <https://www.ansible.com/>`__, `OpenStack
<https://www.openstack.org/>`__ and `Debian GNU/Linux`_. Debian GNU/Linux
based virtual machines are created on an OpenStack tenant and provisioned
with Ansible playbooks.
Enough is based on `Ansible <https://www.ansible.com/>`__, and `Debian
GNU/Linux`_. When running in the cloud it relies on `OpenStack
<https://www.openstack.org/>`__. When running on physical hardware, it
relies on `libvirt <https://libvirt.org/>`__. In both cases the Debian
GNU/Linux based virtual machines are provisioned with Ansible playbooks.
Each service adds more components such as `Let's encrypt
<https://letsencrypt.org/>`__, `Docker <https://www.docker.com/>`__ or
......@@ -44,8 +25,15 @@ about all of the above. If that happens, it is best to start a
`discussion in the forum
<https://forum.enough.community/c/support/5>`__ and ask for help.
Installation
------------
Using the cloud or physical machines
------------------------------------
Enough can be 100% in the cloud (using OpenStack), 100% on physical
machines (using libvirt), or a mixture of both connected together with
a VPN.
Installation of the Enough CLI
------------------------------
* `Install Docker <http://docs.docker.com/engine/installation/>`__.
......@@ -74,10 +62,28 @@ To upgrade to the latest Enough version:
$ docker pull enoughcommunity/enough:latest
$ eval "$(docker run --rm enoughcommunity/enough:latest install)"
OpenStack Enough instance
-------------------------
Requirements
~~~~~~~~~~~~
* The ``clouds.yml`` credentials for either OVH or Fuga.
* The `Orchestration API available <https://wiki.openstack.org/wiki/Heat>`__:
`Heat`, at least ``heat_template_version``: ``2016-10-14``/``newton``.
* A public IPv4 per virtual machine (not a *floating IP* but a *direct IP*).
IPv6 isn't supported yet.
* In order to deploy every available services: 15 virtual machines.
* The flavors must be provided with an attached root disk by default (not an
explicit block storage) most of the virtual machines use 2Go RAM, some
hosts/tests require 4Go or 8Go RAM.
* A `Debian GNU/Linux <https://www.debian.org/>`_ stable image.
.. _bind_create:
Create the DNS name server
--------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~
Assuming the domain name (``example.com`` for example) is registered,
it must be delegated to a dedicated name server before any service can
......@@ -107,9 +113,6 @@ To verify the DNS running at bind-host works as expected:
$ dig @ip-of-the-bind-host bind.example.com
$ enough --domain example.com ssh bind-host
Domain name delegation
~~~~~~~~~~~~~~~~~~~~~~
It can then be used to instruct the `registrar
<https://en.wikipedia.org/wiki/Domain_name_registrar>`__ of
``example.com`` to delegate all domain name resolutions to this
......@@ -130,25 +133,47 @@ To verify the delegation is effective:
getent hosts bind.example.com
Bind configuration
++++++++++++++++++
libvirt Enough instance
-----------------------
Requirements
~~~~~~~~~~~~
* A physical machine with Debian GNU/Linux stable (the IP of the machine
is 192.168.1.19 in the examples below)
* A debian user with passwordless ssh access from the machine where
the Enough CLI is installed:
.. code::
$ enough --domain lan.example.com info
$ ssh-copy-id -i ~/.enough/enough.lan/infrastructure_key.pub debian@192.168.1.19
* Allow debian passwordless sudo access:
If `Bind <https://bind.isc.org/doc/>`__ is used to manage the domain name
``example.com``, the following records need to be added to the related zone
configuration:
.. code::
$ ssh debian@192.168.1.19
$ su
$ echo 'debian ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/debian
Setup libvirt
~~~~~~~~~~~~~
The libvirt daemon and tools must be installed as follows:
.. code::
example.com. IN NS bind.example.com.
bind.example.com. IN A 51.178.60.121
enough --domain lan.example.com libvirt install 192.168.1.19
When a subdomain is used for the infrastructure managed by enough, use these
records instead:
Create the DNS name server
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code::
sub.example.com. IN NS bind.sub.example.com.
bind.sub.example.com. IN A 51.178.60.121
enough --domain lan.example.com service create bind --driver libvirt
Create or update a service
......@@ -176,13 +201,14 @@ The following services are available:
* :doc:`Jitsi <services/jitsi>`, for `video conferencing <https://jitsi.org/>`__ at ``jitsi.example.com``
* :doc:`Psono <services/psono>`, for `password management <https://psono.com/>`__ at ``psono.example.com``
As an example, the cloud service can be created as follows:
As an example, the `cloud` service can be created as follows, with `OpenStack`:
.. code::
enough --domain example.com service create cloud
.. note::
If the command fails, because of a network failure or any other reason,
it is safe to run it again. It is idempotent.
......@@ -190,6 +216,13 @@ When it completes successfully, it is possible to login
``https://cloud.example.com`` with user ``admin`` and password
``mynextcloud``.
If running with `libvirt` instead of `OpenStack`, the `--driver
libvirt` argument must be explicitly provided as follows:
.. code::
enough --domain example.com service create --driver libvirt cloud
Restore a service
-----------------
......@@ -208,8 +241,8 @@ information that are located in a encrypted volume attached to the
machine. A daily :doc:`backup <services/backup>` is made in case a
file is inadvertendly lost.
Infrastructure services and access
----------------------------------
OpenStack infrastructure services and access
--------------------------------------------
Networks
~~~~~~~~
......@@ -268,34 +301,34 @@ created and contains OpenVPN credentials. The specific instructions
to use them depends on the client.
Certificates
~~~~~~~~~~~~
------------
By default certificates are obtained from `Let's Encrypt
<https://letsencrypt.org>`__. But if a host is not publicly
accessible, it can be configured to obtain a certificate from a
certificate authority dedicated to the Enough instance. The default
for `certificate_authority` should be set in
`~/.enough/example.com/inventory/group_vars/all/certificate.yml`, using `this example <https://lab.enough.community/main/infrastructure/blob/master/inventory/group_vars/all/certificate.yml>`__.
<https://letsencrypt.org>`__ when using OpenStack. But if a host is
not publicly accessible, which is the case when using `libvirt`, it
can be configured to obtain a certificate from a certificate authority
dedicated to the Enough instance. The default for
`certificate_authority` should be set in
`~/.enough/example.com/inventory/group_vars/all/certificate.yml`,
using `this example
<https://lab.enough.community/main/infrastructure/blob/master/inventory/group_vars/all/certificate.yml>`__.
The default can also be changed for a given host (for instance
`weblate-host`) by setting the desired value in the
`~/.enough/example.com/inventory/host_vars/weblate-host/network.yml` file.
Renewal
+++++++
When using a certificate authority dedicated to the Enough instance,
each certificate must be manually renewed after a year. For instance,
the certificate of `website.example.com` can be renewed as follows::
the certificate of `website.example.com` can be renewed as follows:
.. code::
$ rm ~/.enough/example.com/certs/website.example.com*
$ enough --domain example.com service create website
$ rm ~/.enough/example.com/certs/website.example.com*
$ enough --domain example.com service create website
The service create is idempotent: it will notice that the certificate
is missing, create a new one, upload it, install it and reload the web
server.
The `service create` command is idempotent: it will notice that the
certificate is missing, create a new one, upload it, install it and
reload the web server.
.. note::
......@@ -304,11 +337,11 @@ server.
.. _attached_volumes:
Attached volumes
~~~~~~~~~~~~~~~~
OpenStack Attached volumes
--------------------------
Provisioning
++++++++++++
~~~~~~~~~~~~
A volume can be created and attached to the host. It can be resized at
a later time, when more space is needed. For instance, before creating
......@@ -325,7 +358,7 @@ file like so:
Encrypting and Mounting
+++++++++++++++++++++++
~~~~~~~~~~~~~~~~~~~~~~~
The volume can then be encrypted, formatted and mounted by specifying
the mount point in the `encrypted_device_mount_point` variable like so:
......@@ -356,7 +389,7 @@ variables like so:
encrypted_volume_for_snap: false
Resizing
++++++++
~~~~~~~~
The size of a volume can be increased (but never decreased) by
modifying the value from (for instance) 10GB
......@@ -394,23 +427,15 @@ the partition.
$ enough --domain example.com ssh weblate-host -- sudo cryptsetup resize --key-file=/etc/cryptsetup/keyfile spare
$ enough --domain example.com ssh weblate-host -- sudo resize2fs /dev/mapper/spare
Services
~~~~~~~~
The following services are always available:
* :doc:`bind <services/bind>` for `DNS server <https://www.isc.org/bind/>`__ at ``bind.examples.com``
* `security groups <https://docs.openstack.org/nova/train/admin/security-groups.html>`__ for :ref:`firewall <firewall>`.
Background tasks
~~~~~~~~~~~~~~~~
----------------
* :doc:`Volumes and hosts backups <services/backup>`.
* `Unattended upgrades <https://wiki.debian.org/UnattendedUpgrades>`__.
* Tracking changes in `/etc/ for each machine <http://source.etckeeper.branchable.com>`__.
Access
~~~~~~
------
The `SSH public keys <https://en.wikipedia.org/wiki/Secure_Shell>`__ found in
files matching ``authorized_keys_globs`` are installed on every machine.
......@@ -422,6 +447,10 @@ files matching ``authorized_keys_globs`` are installed on every machine.
- ssh_keys/dachary.pub
- ssh_keys/glen.pub
OpenStack backups
-----------------
.. _restore_service_from_backup:
Restore a service from a backup
......@@ -607,8 +636,8 @@ should be deleted from these services before being removed.
enough --domain example.com host delete my-host
Running openstack
~~~~~~~~~~~~~~~~~
OpenStack CLI
~~~~~~~~~~~~~
The `openstack <https://docs.openstack.org/python-openstackclient>`__
CLI can be used as follows:
......@@ -625,8 +654,8 @@ Which is exactly equivalent to:
openstack --os-cloud production help
Running ansible-playbook
~~~~~~~~~~~~~~~~~~~~~~~~
Playbook CLI
~~~~~~~~~~~~
The `ansible-playbook <https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html>`__
CLI can be used as follows:
......
from cliff.command import Command
from enough import settings
from enough.common import Enough
class Install(Command):
"Install"
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument('host')
return parser
def take_action(self, parsed_args):
args = vars(self.app.options)
args.update(vars(parsed_args))
args['driver'] = 'libvirt'
e = Enough(settings.CONFIG_DIR, settings.SHARE_DIR, **args)
e.libvirt_install()
......@@ -10,7 +10,7 @@ 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.libvirt import Libvirt, libvirt_install
from enough.common.ssh import SSH
from enough.common import retry
from enough.common import ansible_utils
......@@ -314,3 +314,7 @@ class Enough(object):
self.openstack.volume_prune(self.args['days'])
elif self.args.get('driver') == 'libvirt':
self.libvirt.backup_prune(OpenStack(self.config_dir, **self.args))
def libvirt_install(self):
return libvirt_install(self.config_dir, self.share_dir,
self.args['domain'], self.args['host'])
......@@ -12,6 +12,15 @@ from enough.common import retry
log = logging.getLogger(__name__)
def libvirt_install(config_dir, share_dir, domain, ip):
Hosts(config_dir).create_or_update('libvirt-hypervisor', ip, '22')
playbook = ansible_utils.Playbook(config_dir, share_dir)
playbook.run('--private-key', f'{config_dir}/infrastructure_key',
'--limit', 'libvirt-hypervisor,localhost',
f'{share_dir}/libvirt-hypervisor-playbook.yml')
return True
class Libvirt(object):
BIND_MAC = '52:54:00:00:00:02'
......
---
- import_playbook: "{{ '$SHARE_DIR/playbooks/misc/sexy-debian-playbook.yml' | expandvars }}"
- name: install libvirt
hosts: libvirt-hypervisor
become: true
pre_tasks:
- name: apt-get install libvirt
apt:
name:
- libvirt-clients
- libguestfs-tools
- virtinst
- python3-libvirt
- python3-lxml
- libvirt-dev
- pkg-config
when: ansible_distribution_release == 'bullseye'
- name: apt-get install libvirt
apt:
name:
- libvirt-clients
- libguestfs-tools
- virtinst
- python-libvirt
- python-lxml
- libvirt-dev
- pkg-config
when: ansible_distribution_release == 'buster'
---
- name: setup libvirt locally
hosts: localhost
gather_facts: false
become: true
roles:
- role: libvirt
---
- name: apt-get install libguestfs-tools virtinst python-libvirt python-lxml
apt:
name:
- libguestfs-tools
- virtinst
- python-libvirt
- python-lxml
- libvirt-dev
- pkg-config
......@@ -39,6 +39,7 @@ data_files =
share/enough =
ansible.cfg
copy-playbook.yml
libvirt-hypervisor-playbook.yml
enough-before-playbook.yml
enough-playbook.yml
enough-after-playbook.yml
......@@ -79,6 +80,7 @@ enough.cli =
info = enough.cli.info:Info
playbook = enough.cli.playbook:Playbook
openstack = enough.cli.openstack:Cli
libvirt_install = enough.cli.libvirt:Install
manage = enough.cli.manage:Manage
service_create = enough.cli.service:Create
host_create = enough.cli.host:Create
......
......@@ -4,6 +4,26 @@ import sh
from enough.common import libvirt
from enough.common.openstack import OpenStack
from enough.common.dotenough import Hosts, DotEnoughLibvirt
@pytest.mark.libvirt_integration
def test_libvirt_install(dotenough_libvirt_fixture):
lv = libvirt.Libvirt(dotenough_libvirt_fixture.config_dir, '.',
domain=dotenough_libvirt_fixture.domain)
info = lv.create_or_update([dotenough_libvirt_fixture.prefix])
assert info[dotenough_libvirt_fixture.prefix]['port'] == '22'
hypervisor_ip = info[dotenough_libvirt_fixture.prefix]['ipv4']
assert libvirt.libvirt_install(dotenough_libvirt_fixture.config_dir, '.',
dotenough_libvirt_fixture.domain,
hypervisor_ip) is True
hosts = Hosts(dotenough_libvirt_fixture.config_dir)
dotenough = DotEnoughLibvirt(dotenough_libvirt_fixture.config_dir,
dotenough_libvirt_fixture.domain)
assert hosts.get_ip('libvirt-hypervisor') == hypervisor_ip
assert sh.ssh('-oStrictHostKeyChecking=no',
'-i', dotenough.private_key(), f'debian@{hypervisor_ip}',
'which', 'virsh').exit_code == 0
@pytest.mark.libvirt_integration
......
......@@ -43,6 +43,23 @@ def test_playbook_command(capsys, mocker):
assert 'PLAYBOOK' in out
def test_libvirt_command(capsys, mocker):
# do not tamper with logging streams to avoid
# ValueError: I/O operation on closed file.
mocker.patch('cliff.app.App.configure_logging')
def set_args(self, **kwargs):
self.args = self.init_args.copy()
self.args.update(kwargs)
mocker.patch('enough.common.Enough.set_args', set_args)
mocker.patch('enough.common.libvirt_install',
side_effect=lambda *args: print('LIBVIRT'))
assert cmd.main(['libvirt', 'install', '1.2.3.4']) == 0
out, err = capsys.readouterr()
assert 'LIBVIRT' in out
@pytest.mark.ansible_integration
def test_playbook_execution(mocker, monkeypatch):
monkeypatch.setenv('ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS', 'ignore')
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment