summaryrefslogtreecommitdiffstats
path: root/qa/tasks/dnsmasq.py
blob: df8ccecb1f53cc05432f9f1362474617caf3155e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""
Task for dnsmasq configuration
"""
import contextlib
import logging

from teuthology import misc
from teuthology.exceptions import ConfigError
from teuthology import contextutil
from teuthology import packaging
from tasks.util import get_remote_for_role

log = logging.getLogger(__name__)

@contextlib.contextmanager
def install_dnsmasq(remote):
    """
    If dnsmasq is not installed, install it for the duration of the task.
    """
    try:
        existing = packaging.get_package_version(remote, 'dnsmasq')
    except:
        existing = None

    if existing is None:
        packaging.install_package('dnsmasq', remote)
    try:
        yield
    finally:
        if existing is None:
            packaging.remove_package('dnsmasq', remote)

@contextlib.contextmanager
def backup_resolv(remote, path):
    """
    Store a backup of resolv.conf in the testdir and restore it after the task.
    """
    remote.run(args=['cp', '/etc/resolv.conf', path])
    try:
        yield
    finally:
        # restore with 'cp' to avoid overwriting its security context
        remote.run(args=['sudo', 'cp', path, '/etc/resolv.conf'])
        remote.run(args=['rm', path])

@contextlib.contextmanager
def replace_resolv(remote, path):
    """
    Update resolv.conf to point the nameserver at localhost.
    """
    remote.write_file(path, "nameserver 127.0.0.1\n")
    try:
        # install it
        if remote.os.package_type == "rpm":
            # for centos ovh resolv.conf has immutable attribute set
            remote.run(args=['sudo', 'chattr', '-i', '/etc/resolv.conf'], check_status=False)
        remote.run(args=['sudo', 'cp', path, '/etc/resolv.conf'])
        yield
    finally:
        remote.run(args=['rm', path])

@contextlib.contextmanager
def setup_dnsmasq(remote, testdir, cnames):
    """ configure dnsmasq on the given remote, adding each cname given """
    log.info('Configuring dnsmasq on remote %s..', remote.name)

    # add address entries for each cname
    dnsmasq = "server=8.8.8.8\nserver=8.8.4.4\n"
    address_template = "address=/{cname}/{ip_address}\n"
    for cname, ip_address in cnames.items():
        dnsmasq += address_template.format(cname=cname, ip_address=ip_address)

    # write to temporary dnsmasq file
    dnsmasq_tmp = '/'.join((testdir, 'ceph.tmp'))
    remote.write_file(dnsmasq_tmp, dnsmasq)

    # move into /etc/dnsmasq.d/
    dnsmasq_path = '/etc/dnsmasq.d/ceph'
    remote.run(args=['sudo', 'mv', dnsmasq_tmp, dnsmasq_path])
    # restore selinux context if necessary
    remote.run(args=['sudo', 'restorecon', dnsmasq_path], check_status=False)

    # restart dnsmasq
    remote.run(args=['sudo', 'systemctl', 'restart', 'dnsmasq'])
    # verify dns name is set
    remote.run(args=['ping', '-c', '4', next(iter(cnames.keys()))])

    try:
        yield
    finally:
        log.info('Removing dnsmasq configuration from remote %s..', remote.name)
        # remove /etc/dnsmasq.d/ceph
        remote.run(args=['sudo', 'rm', dnsmasq_path])
        # restart dnsmasq
        remote.run(args=['sudo', 'systemctl', 'restart', 'dnsmasq'])

@contextlib.contextmanager
def task(ctx, config):
    """
    Configures dnsmasq to add cnames for teuthology remotes. The task expects a
    dictionary, where each key is a role. If all cnames for that role use the
    same address as that role, the cnames can be given as a list. For example,
    this entry configures dnsmasq on the remote associated with client.0, adding
    two cnames for the ip address associated with client.0:

        - dnsmasq:
            client.0:
            - client0.example.com
            - c0.example.com

    If the addresses do not all match the given role, a dictionary can be given
    to specify the ip address by its target role. For example:

        - dnsmasq:
            client.0:
              client.0.example.com: client.0
              client.1.example.com: client.1

    Cnames that end with a . are treated as prefix for the existing hostname.
    For example, if the remote for client.0 has a hostname of 'example.com',
    this task will add cnames for dev.example.com and test.example.com:

        - dnsmasq:
            client.0: [dev., test.]
    """
    # apply overrides
    overrides = config.get('overrides', {})
    misc.deep_merge(config, overrides.get('dnsmasq', {}))

    # multiple roles may map to the same remote, so collect names by remote
    remote_names = {}
    for role, cnames in config.items():
        remote = get_remote_for_role(ctx, role)
        if remote is None:
            raise ConfigError('no remote for role %s' % role)

        names = remote_names.get(remote, {})

        if isinstance(cnames, list):
            # when given a list of cnames, point to local ip
            for cname in cnames:
                if cname.endswith('.'):
                    cname += remote.hostname
                names[cname] = remote.ip_address
        elif isinstance(cnames, dict):
            # when given a dict, look up the remote ip for each
            for cname, client in cnames.items():
                r = get_remote_for_role(ctx, client)
                if r is None:
                    raise ConfigError('no remote for role %s' % client)
                if cname.endswith('.'):
                    cname += r.hostname
                names[cname] = r.ip_address

        remote_names[remote] = names

    testdir = misc.get_testdir(ctx)
    resolv_bak = '/'.join((testdir, 'resolv.bak'))
    resolv_tmp = '/'.join((testdir, 'resolv.tmp'))

    # run subtasks for each unique remote
    subtasks = []
    for remote, cnames in remote_names.items():
        subtasks.extend([ lambda r=remote: install_dnsmasq(r) ])
        subtasks.extend([ lambda r=remote: backup_resolv(r, resolv_bak) ])
        subtasks.extend([ lambda r=remote: replace_resolv(r, resolv_tmp) ])
        subtasks.extend([ lambda r=remote, cn=cnames: setup_dnsmasq(r, testdir, cn) ])

    with contextutil.nested(*subtasks):
        yield