Browse Source

enough: add --host option to service create

It is inconvenient to document where and how the service groups must
be updated to specify the host on which a service is deployed when it
is not bound to a specific host. For instance weblate requires
weblate-host to exist and does not require any understanding from the
user. But wekan is not bound to any host and it is up to the user to
choose where it should be deployed. Doing so by modifying services.yml
is error prone. Instead, a --host option is added for the user to
choose the host and the services.yml is modified for them.

Fixes: main/infrastructure#205
keep-around/b8c292a06b6da707f88a262b17d9385a35f4a814
Loïc Dachary 12 months ago
committed by Loic Dachary
parent
commit
b8c292a06b
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 6
      docs/services/VPN.rst
  2. 12
      docs/services/backup.rst
  3. 6
      docs/services/pad.rst
  4. 6
      docs/services/securedrop.rst
  5. 6
      docs/services/wekan.rst
  6. 1
      enough/cli/service.py
  7. 18
      enough/common/dotenough.py
  8. 19
      enough/common/service.py
  9. 5
      inventory/services.yml
  10. 23
      tests/enough/common/test_common_service.py
  11. 22
      tests/enough/common/test_dotenough.py

6
docs/services/VPN.rst

@ -13,6 +13,12 @@ in two ways:
* By connecting to the VPN (which is running on a host connected to
both the public network and the internal network).
The service is created on the host specified by the `--host` argument:
.. code::
$ enough --domain example.com service create --host bind-host openvpn
VPN Server configuration
------------------------

12
docs/services/backup.rst

@ -3,15 +3,13 @@ Backups
Persistent data is placed in :ref:`encrypted volumes
<attached_volumes>` otherwise it may be deleted at any moment, when
the host fails. A daily backup of all volumes is done by the host in
the `backup-service-group` group. Exactly one host must be set in the
`~/.enough/example.com/inventory/services.yml` file, like so:
the host fails. A daily backup of all volumes is done on the host
designated to host backups (for instance `bind-host`) when the service
is created as follows:
.. code:: yaml
.. code::
backup-service-group:
hosts:
bind-host:
$ enough --domain example.com service create --host bind-host backup
The number of backups is defined with the `backup_retention_days` variable
as documented `in this file <https://lab.enough.community/main/infrastructure/blob/master/playbooks/backup/roles/backup/defaults/main.yml>`__ and can be set in `~/.enough/example.com/inventory/group_vars/backup-service-group.yml` like so:

6
docs/services/pad.rst

@ -8,3 +8,9 @@ as documented in `this file
and can be modified in the
`~/.enough/example.com/inventory/group_vars/pad-service-group.yml`
file.
The service is created on the host specified by the `--host` argument:
.. code::
$ enough --domain example.com service create --host website-host pad

6
docs/services/securedrop.rst

@ -9,6 +9,12 @@ and can be modified in the
`~/.enough/example.com/inventory/group_vars/securedrop-service-group.yml`
file.
The service is created on the host specified by the `--host` argument:
.. code::
$ enough --domain example.com service create --host website-host securedrop
URLs
----

6
docs/services/wekan.rst

@ -8,3 +8,9 @@ as documented in `this file
and can be modified in the
`~/.enough/example.com/inventory/group_vars/wekan-service-group.yml`
file.
The service is created on the host specified by the `--host` argument:
.. code::
$ enough --domain example.com service create --host website-host wekan

1
enough/cli/service.py

@ -12,6 +12,7 @@ class Create(ShowOne):
parser = super().get_parser(prog_name)
parser.add_argument('name')
parser.add_argument('--playbook', default='enough-playbook.yml')
parser.add_argument('--host')
return options.set_options(parser)
def take_action(self, parsed_args):

18
enough/common/dotenough.py

@ -101,6 +101,24 @@ class DotEnough(object):
if not os.path.exists(f'{d}/certificate.yml'):
self.set_certificate(certificate_authority)
@staticmethod
def service2group(service):
return f'{service}-service-group'
def service_add_to_group(self, service, host):
s = f'{self.config_dir}/inventory/services.yml'
if os.path.exists(s):
services = yaml.load(open(s).read())
else:
services = {}
group = self.service2group(service)
if group not in services:
services[group] = {'hosts': {}}
if host not in services[group]['hosts']:
services[group]['hosts'][host] = None
open(s, 'w').write(yaml.dump(services, indent=4))
return services
def set_certificate(self, certificate_authority):
d = f'{self.config_dir}/inventory/group_vars/all'
open(f'{d}/certificate.yml', 'w').write(textwrap.dedent(f"""\

19
enough/common/service.py

@ -13,11 +13,17 @@ log = logging.getLogger(__name__)
class Service(object):
class NoHost(Exception):
pass
def __init__(self, config_dir, share_dir, **kwargs):
self.args = kwargs
self.config_dir = config_dir
self.share_dir = share_dir
self.ansible = ansible_utils.Ansible(
self.config_dir, self.share_dir, kwargs.get('inventory', []))
self.dotenough = dotenough.DotEnough(config_dir, self.args['domain'])
self.dotenough.ensure()
self.set_service_info()
self.update_vpn_dependencies()
@ -34,6 +40,18 @@ class Service(object):
if name.endswith(suffix)
}
def ensure_non_empty_service_group(self):
service = self.args['name']
hosts = self.service2group.get(service)
if not hosts and not self.args.get('host'):
raise ServiceOpenStack.NoHost(
f"service {service} needs a host, specify one with --host")
if self.args.get('host'):
self.dotenough.service_add_to_group(service, self.args['host'])
self.set_service_info()
hosts = self.service2group[service]
return hosts
def update_vpn_dependencies(self):
hosts = self.ansible.ansible_inventory()['_meta']['hostvars'].keys()
internal_hosts = set(self.hosts_with_internal_network(hosts))
@ -119,6 +137,7 @@ class ServiceOpenStack(Service):
return True
def create_or_update(self):
self.ensure_non_empty_service_group()
hosts = self.service2hosts[self.args['name']]
h = openstack.Heat(self.config_dir, cloud=self.args['cloud'])
h.create_missings(hosts, self.dotenough.public_key())

5
inventory/services.yml

@ -1,6 +1,11 @@
---
essential-service-group:
hosts:
bind-host:
essential-service-hosts:
children:
essential-service-group:
bind-service-group:
postfix-service-group:
icinga-service-group:

23
tests/enough/common/test_common_service.py

@ -32,21 +32,21 @@ def test_openstack_create_or_update(tmpdir, openstack_name, requests_mock):
def test_service_from_host():
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR)
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR, domain='test.com')
assert s.service_from_host('icinga-host') in ('essential', 'openvpn', 'wekan')
assert s.service_from_host('cloud-host') == 'cloud'
assert s.service_from_host('unknown-host') is None
def test_set_service_info():
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR)
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR, domain='test.com')
assert 'bind-host' in s.service2hosts['bind']
assert len(s.service2hosts['bind']) > 0
assert ['bind-host'] == s.service2group['bind']
def test_update_vpn_dependencies():
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR)
s = service.Service(settings.CONFIG_DIR, settings.SHARE_DIR, domain='test.com')
assert s.hosts_with_internal_network(['bind-host']) == []
assert 'website-host' not in s.service2hosts['openvpn']
assert 'website-host' not in s.service2hosts['weblate']
@ -56,3 +56,20 @@ def test_update_vpn_dependencies():
assert s.hosts_with_internal_network(['icinga-host']) == ['icinga-host']
s.update_vpn_dependencies()
assert 'website-host' in s.service2hosts['weblate']
def test_ensure_non_empty_service_group(tmpdir):
name = 'wekan'
s = service.Service(tmpdir, settings.SHARE_DIR,
name=name,
domain='test.com')
with pytest.raises(service.Service.NoHost):
s.ensure_non_empty_service_group()
host = 'HOST'
s = service.Service(tmpdir, settings.SHARE_DIR,
name=name,
host=host,
domain='test.com')
hosts = s.ensure_non_empty_service_group()
assert hosts == [host]
assert hosts == s.ensure_non_empty_service_group()

22
tests/enough/common/test_dotenough.py

@ -1,7 +1,7 @@
import os
import yaml
from enough.common.dotenough import Hosts, DotEnoughOpenStack
from enough.common.dotenough import Hosts, DotEnough, DotEnoughOpenStack
#
@ -31,6 +31,26 @@ def test_hosts_create_delete(tmpdir):
#
# DotEnough
#
def test_service_add_to_group(tmpdir):
d = DotEnough(tmpdir, 'test.com')
service = 'SERVICE'
group = DotEnough.service2group(service)
host = 'HOST'
expected = {
group: {
'hosts': {
host: None,
}
}
}
assert d.service_add_to_group(service, host) == expected
assert d.service_add_to_group(service, host) == expected
other_host = 'OTHER'
expected[group]['hosts'][other_host] = None
assert d.service_add_to_group(service, other_host) == expected
os.system(f'cat {tmpdir}/services.yml')
def test_openstack_openrc2clouds(tmpdir):
openrc_file = f'{tmpdir}/openrc.sh'

Loading…
Cancel
Save