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