Home | History | Annotate | Download | only in peerd_AdvertiseServices
      1 # Copyright 2014 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import dpkt
      6 import logging
      7 import re
      8 
      9 from autotest_lib.client.bin import test
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.client.common_lib.cros.tendo import peerd_config
     12 from autotest_lib.client.cros import chrooted_avahi
     13 from autotest_lib.client.cros.netprotos import interface_host
     14 from autotest_lib.client.cros.netprotos import zeroconf
     15 from autotest_lib.client.cros.tendo import peerd_dbus_helper
     16 
     17 
     18 class peerd_AdvertiseServices(test.test):
     19     """Test that peerd can correctly advertise services over mDNS."""
     20     version = 1
     21 
     22     ANY_VALUE = object()  # Use reference equality for wildcard.
     23     FAKE_HOST_HOSTNAME = 'test-host'
     24     TEST_TIMEOUT_SECONDS = 30
     25     TEST_SERVICE_ID = 'test-service-0'
     26     TEST_SERVICE_INFO = {'some_data': 'a value',
     27                           'other_data': 'another value'}
     28     TEST_SERVICE_PORT = 8080
     29     SERBUS_SERVICE_ID = 'serbus'
     30     SERBUS_SERVICE_INFO = {
     31             'ver': '1.0',
     32             'id': ANY_VALUE,
     33             'services': r'(.+\.)?' + TEST_SERVICE_ID + r'(\..+)?',
     34     }
     35     SERBUS_SERVICE_PORT = 0
     36 
     37 
     38     def initialize(self):
     39         # Make sure these are initiallized to None in case we throw
     40         # during self.initialize().
     41         self._chrooted_avahi = None
     42         self._peerd = None
     43         self._host = None
     44         self._zc_listener = None
     45         self._chrooted_avahi = chrooted_avahi.ChrootedAvahi()
     46         self._chrooted_avahi.start()
     47         # Start up a cleaned up peerd with really verbose logging.
     48         self._peerd = peerd_dbus_helper.make_helper(
     49                 peerd_config.PeerdConfig(verbosity_level=3))
     50         # Listen on our half of the interface pair for mDNS advertisements.
     51         self._host = interface_host.InterfaceHost(
     52                 self._chrooted_avahi.unchrooted_interface_name)
     53         self._zc_listener = zeroconf.ZeroconfDaemon(self._host,
     54                                                     self.FAKE_HOST_HOSTNAME)
     55         # The queries for hostname/dns_domain are IPCs and therefor relatively
     56         # expensive.  Do them just once.
     57         hostname = self._chrooted_avahi.hostname
     58         dns_domain = self._chrooted_avahi.dns_domain
     59         if not hostname or not dns_domain:
     60             raise error.TestFail('Failed to get hostname/domain from avahi.')
     61         self._dns_domain = dns_domain
     62         self._hostname = '%s.%s' % (hostname, dns_domain)
     63 
     64 
     65     def cleanup(self):
     66         for obj in (self._chrooted_avahi,
     67                     self._host,
     68                     self._peerd):
     69             if obj is not None:
     70                 obj.close()
     71 
     72 
     73     def _check_txt_record_data(self, expected_data, actual_data):
     74         # Labels in the TXT record should be 1:1 with our service info.
     75         expected_entries = expected_data.copy()
     76         for entry in actual_data:
     77             # All labels should be key/value pairs.
     78             if entry.find('=') < 0:
     79                 raise error.TestFail('All TXT entries should have = separator, '
     80                                      'but got: %s' % entry)
     81             k, v = entry.split('=', 1)
     82             if k not in expected_entries:
     83                 raise error.TestFail('Unexpected TXT entry key: %s' % k)
     84             if (expected_entries[k] != self.ANY_VALUE and
     85                     not re.match(expected_entries[k], v)):
     86                 # We're going to return False here rather than fail the test
     87                 # for one tricky reason: in the root serbus record, we may
     88                 # find that the service list does not match our expectation
     89                 # since other daemons may be advertising services via peerd.
     90                 # We need to basically wait for our test service to show up.
     91                 logging.warning('Expected TXT value to match %s for '
     92                                 'entry=%s but got value=%r instead.',
     93                                 expected_entries[k], k, v)
     94                 return False
     95             expected_entries.pop(k)
     96         if expected_entries:
     97             # Raise a detailed exception here, rather than return false.
     98             raise error.TestFail('Missing entries from TXT: %r' %
     99                                  expected_entries)
    100         return True
    101 
    102 
    103     def _ask_for_record(self, record_name, record_type):
    104         """Ask for a record, and query for it if we don't have it.
    105 
    106         @param record_name: string name of record (e.g. the complete host name
    107                             for A records.
    108         @param record_type: one of dpkt.dns.DNS_*.
    109         @return list of matching records.
    110 
    111         """
    112         found_records = self._zc_listener.cached_results(
    113                 record_name, record_type)
    114         if len(found_records) > 1:
    115             logging.warning('Found multiple records with name=%s and type=%r',
    116                             record_name, record_type)
    117         if found_records:
    118             logging.debug('Found record with name=%s, type=%r, value=%r.',
    119                           record_name, record_type, found_records[0].data)
    120             return found_records[0]
    121         logging.debug('Did not see record with name=%s and type=%r',
    122                       record_name, record_type)
    123         desired_records = [(record_name, record_type)]
    124         self._zc_listener.send_request(desired_records)
    125         return None
    126 
    127 
    128     def _found_service_records(self, service_id, service_info, service_port):
    129         PTR_name = '_%s._tcp.%s' % (service_id, self._dns_domain)
    130         record_PTR = self._ask_for_record(PTR_name, dpkt.dns.DNS_PTR)
    131         if not record_PTR:
    132             return False
    133         # Great, we know the PTR, make sure that we can also get the SRV and
    134         # TXT entries.
    135         TXT_name = SRV_name = record_PTR.data
    136         record_SRV = self._ask_for_record(SRV_name, dpkt.dns.DNS_SRV)
    137         if record_SRV is None:
    138             return False
    139         if (record_SRV.data[0] != self._hostname or
    140                 record_SRV.data[3] != service_port):
    141             raise error.TestFail('Expected SRV record data %r but got %r' %
    142                                  ((self._hostname, service_port),
    143                                   record_SRV.data))
    144         # TXT should exist.
    145         record_TXT = self._ask_for_record(TXT_name, dpkt.dns.DNS_TXT)
    146         if (record_TXT is None or
    147                 not self._check_txt_record_data(service_info, record_TXT.data)):
    148             return False
    149         return True
    150 
    151 
    152     def _found_desired_records(self):
    153         """Verifies that avahi has all the records we care about.
    154 
    155         Asks the |self._zc_listener| for records we expect to correspond
    156         to our test service.  Will trigger queries if we don't find the
    157         expected records.
    158 
    159         @return True if we have all expected records, False otherwise.
    160 
    161         """
    162         logging.debug('Looking for records for %s.', self._hostname)
    163         # First, check that Avahi is doing the simple things and publishing
    164         # an A record.
    165         record_A = self._ask_for_record(self._hostname, dpkt.dns.DNS_A)
    166         if (record_A is None or
    167                 record_A.data != self._chrooted_avahi.avahi_interface_addr):
    168             return False
    169         logging.debug('Found A record, looking for serbus records.')
    170         # If we can see Avahi publishing that it's there, check that it has
    171         # appropriate entries for its serbus master record.
    172         if not self._found_service_records(self.SERBUS_SERVICE_ID,
    173                                            self.SERBUS_SERVICE_INFO,
    174                                            self.SERBUS_SERVICE_PORT):
    175             return False
    176         logging.debug('Found serbus records, looking for service records.')
    177         # We also expect the subservices we've added to exist.
    178         if not self._found_service_records(self.TEST_SERVICE_ID,
    179                                            self.TEST_SERVICE_INFO,
    180                                            self.TEST_SERVICE_PORT):
    181             return False
    182         logging.debug('Found all desired records.')
    183         return True
    184 
    185 
    186     def run_once(self):
    187         # Tell peerd about this exciting new service we have.
    188         self._peerd.expose_service(
    189                 self.TEST_SERVICE_ID,
    190                 self.TEST_SERVICE_INFO,
    191                 mdns_options={'port': self.TEST_SERVICE_PORT})
    192         # Wait for advertisements of that service to appear from avahi.
    193         logging.info('Waiting to receive mDNS advertisements of '
    194                      'peerd services.')
    195         success, duration = self._host.run_until(self._found_desired_records,
    196                                                  self.TEST_TIMEOUT_SECONDS)
    197         if not success:
    198             raise error.TestFail('Did not receive mDNS advertisements in time.')
    199