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