~andreserl/maas/lp1592666

« back to all changes in this revision

Viewing changes to src/provisioningserver/dhcp/detect.py

  • Committer: LaMont Jones
  • Date: 2016-04-11 16:23:26 UTC
  • mfrom: (4900 maas)
  • mto: This revision was merged to the branch mainline in revision 4924.
  • Revision ID: lamont@canonical.com-20160411162326-6ycj8l2j66v2o5es
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
1
2
# Copyright 2013-2016 Canonical Ltd.  This software is licensed under the
2
3
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
 
10
11
from contextlib import contextmanager
11
12
import errno
12
13
import fcntl
13
 
import http.client
14
 
import json
15
14
from random import randint
16
15
import socket
17
16
import struct
18
 
from urllib.error import (
19
 
    HTTPError,
20
 
    URLError,
21
 
)
22
17
 
23
 
from apiclient.maas_client import (
24
 
    MAASClient,
25
 
    MAASDispatcher,
26
 
    MAASOAuth,
27
 
)
28
18
from provisioningserver.logger import get_maas_logger
29
19
 
30
20
 
204
194
    return receive_offers(transaction_id)
205
195
 
206
196
 
207
 
def process_request(client_func, *args, **kwargs):
208
 
    """Run a MAASClient query and check for common errors.
209
 
 
210
 
    :return: None if there is an error, otherwise the decoded response body.
211
 
    """
212
 
    try:
213
 
        response = client_func(*args, **kwargs)
214
 
    except (HTTPError, URLError) as e:
215
 
        maaslog.warning("Failed to contact region controller:\n%s", e)
216
 
        return None
217
 
    code = response.getcode()
218
 
    if code != http.client.OK:
219
 
        maaslog.error(
220
 
            "Failed talking to region controller, it returned:\n%s\n%s",
221
 
            code, response.read())
222
 
        return None
223
 
    try:
224
 
        raw_data = response.read()
225
 
        if len(raw_data) > 0:
226
 
            data = json.loads(raw_data)
227
 
        else:
228
 
            return None
229
 
    except ValueError as e:
230
 
        maaslog.error(
231
 
            "Failed to decode response from region controller:\n%s", e)
232
 
        return None
233
 
    return data
234
 
 
235
 
 
236
 
def determine_cluster_interfaces(knowledge):
237
 
    """Given server knowledge, determine network interfaces on this cluster.
238
 
 
239
 
    :return: a list of tuples of (interface name, ip) for all interfaces.
240
 
 
241
 
    :note: this uses an API call and not local probing because the
242
 
        region controller has the definitive and final say in what does and
243
 
        doesn't exist.
244
 
    """
245
 
    api_path = (
246
 
        'api/2.0/nodegroups/%s/interfaces/' % knowledge['nodegroup_uuid'])
247
 
    oauth = MAASOAuth(*knowledge['api_credentials'])
248
 
    client = MAASClient(oauth, MAASDispatcher(), knowledge['maas_url'])
249
 
    interfaces = process_request(client.get, api_path, 'list')
250
 
    if interfaces is None:
251
 
        return None
252
 
 
253
 
    interface_names = sorted(
254
 
        (interface['interface'], interface['ip'])
255
 
        for interface in interfaces
256
 
        if interface['interface'] != '')
257
 
    return interface_names
258
 
 
259
 
 
260
 
def probe_interface(interface, ip):
 
197
def probe_interface(interface):
261
198
    """Probe the given interface for DHCP servers.
262
199
 
263
 
    :param interface: interface as returned from determine_cluster_interfaces
264
 
    :param ip: ip as returned from determine_cluster_interfaces
 
200
    :param interface: interface name
265
201
    :return: A set of IP addresses of detected servers.
266
202
 
267
203
    :note: Any servers running on the IP address of the local host are
287
223
                "your cluster interfaces configuration.", interface)
288
224
        else:
289
225
            raise
290
 
    # Using servers.discard(ip) here breaks Mock in the tests, so
291
 
    # we're creating a copy of the set instead.
292
 
    results = servers.difference([ip])
293
 
    return results
294
 
 
295
 
 
296
 
def update_region_controller(knowledge, interface, server):
297
 
    """Update the region controller with the status of the probe.
298
 
 
299
 
    :param knowledge: dictionary of server info
300
 
    :param interface: name of interface, e.g. eth0
301
 
    :param server: IP address of detected DHCP server, or None
302
 
    """
303
 
    api_path = 'api/2.0/nodegroups/%s/interfaces/%s/' % (
304
 
        knowledge['nodegroup_uuid'], interface)
305
 
    oauth = MAASOAuth(*knowledge['api_credentials'])
306
 
    client = MAASClient(oauth, MAASDispatcher(), knowledge['maas_url'])
307
 
    if server is None:
308
 
        server = ''
309
 
    process_request(
310
 
        client.post, api_path, 'report_foreign_dhcp', foreign_dhcp_ip=server)
311
 
 
312
 
 
313
 
def periodic_probe_task(api_knowledge):
314
 
    """Probe for DHCP servers and set NodeGroupInterface.foriegn_dhcp.
315
 
 
316
 
    This should be run periodically so that the database has an up-to-date
317
 
    view of any rogue DHCP servers on the network.
318
 
 
319
 
    NOTE: This uses blocking I/O with sequential polling of interfaces, and
320
 
    hence doesn't scale well.  It's a future improvement to make
321
 
    to throw it in parallel threads or async I/O.
322
 
 
323
 
    :param api_knowledge: A dict of the information needed to be able to
324
 
        make requests to the region's REST API.
325
 
    """
326
 
    # Determine all the active interfaces on this cluster (nodegroup).
327
 
    interfaces = determine_cluster_interfaces(api_knowledge)
328
 
    if interfaces is None:
329
 
        maaslog.info("No interfaces on cluster, not probing DHCP.")
330
 
        return
331
 
 
332
 
    # Iterate over interfaces and probe each one.
333
 
    for interface, ip in interfaces:
334
 
        try:
335
 
            servers = probe_interface(interface, ip)
336
 
        except socket.error:
337
 
            maaslog.error(
338
 
                "Failed to probe sockets; did you configure authbind as per "
339
 
                "HACKING.txt?")
340
 
            return
341
 
        else:
342
 
            if len(servers) > 0:
343
 
                # Only send one, if it gets cleared out then the
344
 
                # next detection pass will send a different one, if it
345
 
                # still exists.
346
 
                update_region_controller(
347
 
                    api_knowledge, interface, servers.pop())
348
 
            else:
349
 
                update_region_controller(api_knowledge, interface, None)
 
226
    return servers