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
5import logging
6import time
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.common_lib.cros import avahi_utils
10from autotest_lib.client.common_lib.cros import virtual_ethernet_pair
11from autotest_lib.client.common_lib.cros.network import netblock
12from autotest_lib.client.cros import network_chroot
13from autotest_lib.client.cros import service_stopper
14from autotest_lib.client.cros import tcpdump
15
16
17class ChrootedAvahi(object):
18    """Helper object to start up avahi in a network chroot.
19
20    Creates a virtual ethernet pair to enable communication with avahi.
21    Does the necessary work to make avahi appear on DBus and allow it
22    to claim its canonical service name.
23
24    """
25
26    SERVICES_TO_STOP = ['avahi']
27    # This side has to be called something special to avoid shill touching it.
28    MONITOR_IF_IP = netblock.from_addr('10.9.8.1/24')
29    # We'll drop the Avahi side into our network namespace.
30    AVAHI_IF_IP = netblock.from_addr('10.9.8.2/24')
31    AVAHI_IF_NAME = 'pseudoethernet0'
32    TCPDUMP_FILE_PATH = '/var/log/peerd_dump.pcap'
33    AVAHI_CONFIG_FILE = 'etc/avahi/avahi-daemon.conf'
34    AVAHI_CONFIGS = {
35        AVAHI_CONFIG_FILE :
36            '[server]\n'
37                'host-name-from-machine-id=yes\n'
38                'browse-domains=\n'
39                'use-ipv4=yes\n'
40                'use-ipv6=no\n'
41                'ratelimit-interval-usec=1000000\n'
42                'ratelimit-burst=1000\n'
43            '[wide-area]\n'
44                'enable-wide-area=no\n'
45            '[publish]\n'
46                'publish-hinfo=no\n'
47                'publish-workstation=no\n'
48                'publish-aaaa-on-ipv4=no\n'
49                'publish-a-on-ipv6=no\n'
50            '[rlimits]\n'
51                'rlimit-core=0\n'
52                'rlimit-data=4194304\n'
53                'rlimit-fsize=1024\n'
54                'rlimit-nofile=768\n'
55                'rlimit-stack=4194304\n'
56                'rlimit-nproc=10\n',
57
58        'etc/passwd' :
59            'root:x:0:0:root:/root:/bin/bash\n'
60            'avahi:*:238:238::/dev/null:/bin/false\n',
61
62        'etc/group' :
63            'avahi:x:238:\n',
64    }
65    AVAHI_LOG_FILE = '/var/log/avahi.log'
66    AVAHI_PID_FILE = 'var/run/avahi-daemon/pid'
67    AVAHI_UP_TIMEOUT_SECONDS = 10
68
69
70    def __init__(self, unchrooted_interface_name='pseudoethernet1'):
71        """Construct a chrooted instance of Avahi.
72
73        @param unchrooted_interface_name: string name of interface to leave
74                outside the network chroot.  This interface will be connected
75                to the end Avahi is listening on.
76
77        """
78        self._unchrooted_interface_name = unchrooted_interface_name
79        self._services = None
80        self._vif = None
81        self._tcpdump = None
82        self._chroot = None
83
84
85    @property
86    def unchrooted_interface_name(self):
87        """Get the name of the end of the VirtualEthernetPair not in the chroot.
88
89        The network chroot works by isolating avahi inside with one end of a
90        virtual ethernet pair.  The outside world needs to interact with the
91        other end in order to talk to avahi.
92
93        @return name of interface not inside the chroot.
94
95        """
96        return self._unchrooted_interface_name
97
98
99    @property
100    def avahi_interface_addr(self):
101        """@return string ip address of interface belonging to avahi."""
102        return self.AVAHI_IF_IP.addr
103
104
105    @property
106    def hostname(self):
107        """@return string hostname claimed by avahi on |self.dns_domain|."""
108        return avahi_utils.avahi_get_hostname()
109
110
111    @property
112    def dns_domain(self):
113        """@return string DNS domain in use by avahi (e.g. 'local')."""
114        return avahi_utils.avahi_get_domain_name()
115
116
117    def start(self):
118        """Start up the chrooted Avahi instance."""
119        # Prevent weird interactions between services which talk to Avahi.
120        # TODO(wiley) Does Chrome need to die here as well?
121        self._services = service_stopper.ServiceStopper(
122                self.SERVICES_TO_STOP)
123        self._services.stop_services()
124        # We don't want Avahi talking to the real world, so give it a nice
125        # fake interface to use.  We'll watch the other half of the pair.
126        self._vif = virtual_ethernet_pair.VirtualEthernetPair(
127                interface_name=self.unchrooted_interface_name,
128                peer_interface_name=self.AVAHI_IF_NAME,
129                interface_ip=self.MONITOR_IF_IP.netblock,
130                peer_interface_ip=self.AVAHI_IF_IP.netblock,
131                # Moving one end into the chroot causes errors.
132                ignore_shutdown_errors=True)
133        self._vif.setup()
134        if not self._vif.is_healthy:
135            raise error.TestError('Failed to setup virtual ethernet pair.')
136        # By default, take a packet capture of everything Avahi sends out.
137        self._tcpdump = tcpdump.Tcpdump(self.unchrooted_interface_name,
138                                        self.TCPDUMP_FILE_PATH)
139        # We're going to run Avahi in a network namespace to avoid interactions
140        # with the outside world.
141        self._chroot = network_chroot.NetworkChroot(self.AVAHI_IF_NAME,
142                                                    self.AVAHI_IF_IP.addr,
143                                                    self.AVAHI_IF_IP.prefix_len)
144        self._chroot.add_config_templates(self.AVAHI_CONFIGS)
145        self._chroot.add_root_directories(['etc/avahi', 'etc/avahi/services'])
146        self._chroot.add_copied_config_files(['etc/resolv.conf',
147                                              'etc/avahi/hosts'])
148        self._chroot.add_startup_command(
149                '/usr/sbin/avahi-daemon --file=/%s >%s 2>&1' %
150                (self.AVAHI_CONFIG_FILE, self.AVAHI_LOG_FILE))
151        self._chroot.bridge_dbus_namespaces()
152        self._chroot.startup()
153        # Wait for Avahi to come up, claim its DBus name, settle on a hostname.
154        start_time = time.time()
155        while time.time() - start_time < self.AVAHI_UP_TIMEOUT_SECONDS:
156            if avahi_utils.avahi_ping():
157                break
158            time.sleep(0.2)
159        else:
160            raise error.TestFail('Avahi did not come up in time.')
161
162
163    def close(self):
164        """Clean up the chrooted Avahi instance."""
165        if self._chroot:
166            # TODO(wiley) This is sloppy.  Add a helper to move the logs over.
167            for line in self._chroot.get_log_contents().splitlines():
168                logging.debug(line)
169            self._chroot.kill_pid_file(self.AVAHI_PID_FILE)
170            self._chroot.shutdown()
171        if self._tcpdump:
172            self._tcpdump.stop()
173        if self._vif:
174            self._vif.teardown()
175        if self._services:
176            self._services.restore_services()
177