Home | History | Annotate | Download | only in cros
      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 ConfigParser
      6 import io
      7 import collections
      8 import logging
      9 import shlex
     10 import time
     11 
     12 from autotest_lib.client.bin import utils
     13 from autotest_lib.client.common_lib import error
     14 from autotest_lib.client.common_lib.cros import dbus_send
     15 
     16 BUS_NAME = 'org.freedesktop.Avahi'
     17 INTERFACE_SERVER = 'org.freedesktop.Avahi.Server'
     18 
     19 ServiceRecord = collections.namedtuple(
     20         'ServiceRecord',
     21         ['interface', 'protocol', 'name', 'record_type', 'domain',
     22          'hostname', 'address', 'port', 'txt'])
     23 
     24 
     25 def avahi_config(options, src_file='/etc/avahi/avahi-daemon.conf', host=None):
     26     """Creates a temporary avahi-daemon.conf file with the specified changes.
     27 
     28     Avahi daemon uses a text configuration file with sections and values
     29     assigned to options on that section. This function creates a new config
     30     file based on the one provided and a set of changes. The changes are
     31     specified as triples of section, option and value that override the existing
     32     options on the config file. If a value of None is specified for any triplet,
     33     the corresponding option will be removed from the file.
     34 
     35     @param options: A list of triplets of the form (section, option, value).
     36     @param src_file: The default config file to use as a base for the changes.
     37     @param host: An optional host object if running against a remote host.
     38     @return: The filename of a temporary file with the new configuration file.
     39 
     40     """
     41     run = utils.run if host is None else host.run
     42     existing_config = run('cat %s 2> /dev/null' % src_file).stdout
     43     conf = ConfigParser.SafeConfigParser()
     44     conf.readfp(io.BytesIO(existing_config))
     45 
     46     for section, option, value in options:
     47         if not conf.has_section(section):
     48             conf.add_section(section)
     49         if value is None:
     50             conf.remove_option(section, option)
     51         else:
     52             conf.set(section, option, value)
     53 
     54     tmp_conf_file = run('mktemp -t avahi-conf.XXXX').stdout.strip()
     55     lines = []
     56     for section in conf.sections():
     57         lines.append('[%s]' % section)
     58         for option in conf.options(section):
     59             lines.append('%s=%s' % (option, conf.get(section, option)))
     60     run('cat <<EOF >%s\n%s\nEOF\n' % (tmp_conf_file, '\n'.join(lines)))
     61     return tmp_conf_file
     62 
     63 
     64 def avahi_ping(host=None):
     65     """Returns True when the avahi-deamon's DBus interface is ready.
     66 
     67     After your launch avahi-daemon, there is a short period of time where the
     68     daemon is running but the DBus interface isn't ready yet. This functions
     69     blocks for a few seconds waiting for a ping response from the DBus API
     70     and returns wether it got a response.
     71 
     72     @param host: An optional host object if running against a remote host.
     73     @return boolean: True if Avahi is up and in a stable state.
     74 
     75     """
     76     result = dbus_send.dbus_send(BUS_NAME, INTERFACE_SERVER, '/', 'GetState',
     77                                  host=host, timeout_seconds=2,
     78                                  tolerate_failures=True)
     79     # AVAHI_ENTRY_GROUP_ESTABLISHED == 2
     80     return result is not None and result.response == 2
     81 
     82 
     83 def avahi_start(config_file=None, host=None):
     84     """Start avahi-daemon with the provided config file.
     85 
     86     This function waits until the avahi-daemon is ready listening on the DBus
     87     interface. If avahi fails to be ready after 10 seconds, an error is raised.
     88 
     89     @param config_file: The filename of the avahi-daemon config file or None to
     90             use the default.
     91     @param host: An optional host object if running against a remote host.
     92 
     93     """
     94     run = utils.run if host is None else host.run
     95     env = ''
     96     if config_file is not None:
     97         env = ' AVAHI_DAEMON_CONF="%s"' % config_file
     98     run('start avahi %s' % env, ignore_status=False)
     99     # Wait until avahi is ready.
    100     deadline = time.time() + 10.
    101     while time.time() < deadline:
    102         if avahi_ping(host=host):
    103             return
    104         time.sleep(0.1)
    105     raise error.TestError('avahi-daemon is not ready after 10s running.')
    106 
    107 
    108 def avahi_stop(ignore_status=False, host=None):
    109     """Stop the avahi daemon.
    110 
    111     @param ignore_status: True to ignore failures while stopping avahi.
    112     @param host: An optional host object if running against a remote host.
    113 
    114     """
    115     run = utils.run if host is None else host.run
    116     run('stop avahi', ignore_status=ignore_status)
    117 
    118 
    119 def avahi_start_on_iface(iface, host=None):
    120     """Starts avahi daemon listening only on the provided interface.
    121 
    122     @param iface: A string with the interface name.
    123     @param host: An optional host object if running against a remote host.
    124 
    125     """
    126     run = utils.run if host is None else host.run
    127     opts = [('server', 'allow-interfaces', iface),
    128             ('server', 'deny-interfaces', None)]
    129     conf = avahi_config(opts, host=host)
    130     avahi_start(config_file=conf, host=host)
    131     run('rm %s' % conf)
    132 
    133 
    134 def avahi_get_hostname(host=None):
    135     """Get the lan-unique hostname of the the device.
    136 
    137     @param host: An optional host object if running against a remote host.
    138     @return string: the lan-unique hostname of the DUT.
    139 
    140     """
    141     result = dbus_send.dbus_send(
    142             BUS_NAME, INTERFACE_SERVER, '/', 'GetHostName',
    143             host=host, timeout_seconds=2, tolerate_failures=True)
    144     return None if result is None else result.response
    145 
    146 
    147 def avahi_get_domain_name(host=None):
    148     """Get the current domain name being used by Avahi.
    149 
    150     @param host: An optional host object if running against a remote host.
    151     @return string: the current domain name being used by Avahi.
    152 
    153     """
    154     result = dbus_send.dbus_send(
    155             BUS_NAME, INTERFACE_SERVER, '/', 'GetDomainName',
    156             host=host, timeout_seconds=2, tolerate_failures=True)
    157     return None if result is None else result.response
    158 
    159 
    160 def avahi_browse(host=None, ignore_local=True):
    161     """Browse mDNS service records with avahi-browse.
    162 
    163     Some example avahi-browse output (lines are wrapped for readability):
    164 
    165     localhost ~ # avahi-browse -tarlp
    166     +;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_serbus._tcp;local
    167     +;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_privet._tcp;local
    168     =;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_serbus._tcp;local;\
    169         9bcd92bbc1f91f2ee9c9b2e754cfd22e.local;172.22.23.237;0;\
    170         "ver=1.0" "services=privet" "id=11FB0AD6-6C87-433E-8ACB-0C68EE78CDBD"
    171     =;eth1;IPv4;E58E8561-3BCA-4910-ABC7-BD8779D7D761;_privet._tcp;local;\
    172         9bcd92bbc1f91f2ee9c9b2e754cfd22e.local;172.22.23.237;8080;\
    173         "ty=Unnamed Device" "txtvers=3" "services=_camera" "model_id=///" \
    174         "id=FEE9B312-1F2B-4B9B-813C-8482FA75E0DB" "flags=AB" "class=BB"
    175 
    176     @param host: An optional host object if running against a remote host.
    177     @param ignore_local: boolean True to ignore local service records.
    178     @return list of ServiceRecord objects parsed from output.
    179 
    180     """
    181     run = utils.run if host is None else host.run
    182     flags = ['--terminate',  # Terminate after looking for a short time.
    183              '--all',  # Show all services, regardless of type.
    184              '--resolve',  # Resolve the services discovered.
    185              '--parsable',  # Print service records in a parsable format.
    186     ]
    187     if ignore_local:
    188         flags.append('--ignore-local')
    189     result = run('avahi-browse %s' % ' '.join(flags))
    190     records = []
    191     for line in result.stdout.strip().splitlines():
    192         parts = line.split(';')
    193         if parts[0] == '+':
    194             # Skip it, just parse the resolved record.
    195             continue
    196         # Do minimal parsing of the TXT record.
    197         parts[-1] = shlex.split(parts[-1])
    198         records.append(ServiceRecord(*parts[1:]))
    199         logging.debug('Found %r', records[-1])
    200     return records
    201