• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#   Copyright 2016 - The Android Open Source Project
2#
3#   Licensed under the Apache License, Version 2.0 (the "License");
4#   you may not use this file except in compliance with the License.
5#   You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#   Unless required by applicable law or agreed to in writing, software
10#   distributed under the License is distributed on an "AS IS" BASIS,
11#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#   See the License for the specific language governing permissions and
13#   limitations under the License.
14
15import time
16from retry import retry
17
18from acts.controllers.utils_lib.commands import shell
19from acts import logger
20
21
22class Error(Exception):
23    """An error caused by the dhcp server."""
24
25
26class NoInterfaceError(Exception):
27    """Error thrown when the dhcp server has no interfaces on any subnet."""
28
29
30class DhcpServer(object):
31    """Manages the dhcp server program.
32
33    Only one of these can run in an environment at a time.
34
35    Attributes:
36        config: The dhcp server configuration that is being used.
37    """
38
39    PROGRAM_FILE = 'dhcpd'
40
41    def __init__(self, runner, interface, working_dir='/tmp'):
42        """
43        Args:
44            runner: Object that has a run_async and run methods for running
45                    shell commands.
46            interface: string, The name of the interface to use.
47            working_dir: The directory to work out of.
48        """
49        self._log = logger.create_logger(lambda msg: '[DHCP Server|%s] %s' % (
50            interface, msg))
51        self._runner = runner
52        self._working_dir = working_dir
53        self._shell = shell.ShellCommand(runner, working_dir)
54        self._stdio_log_file = 'dhcpd_%s.log' % interface
55        self._config_file = 'dhcpd_%s.conf' % interface
56        self._lease_file = 'dhcpd_%s.leases' % interface
57        self._pid_file = 'dhcpd_%s.pid' % interface
58        self._identifier = '%s.*%s' % (self.PROGRAM_FILE, self._config_file)
59
60    # There is a slight timing issue where if the proc filesystem in Linux
61    # doesn't get updated in time as when this is called, the NoInterfaceError
62    # will happening.  By adding this retry, the error appears to have gone away
63    # but will still show a warning if the problem occurs.  The error seems to
64    # happen more with bridge interfaces than standard interfaces.
65    @retry(exceptions=NoInterfaceError, tries=3, delay=1)
66    def start(self, config, timeout=60):
67        """Starts the dhcp server.
68
69        Starts the dhcp server daemon and runs it in the background.
70
71        Args:
72            config: dhcp_config.DhcpConfig, Configs to start the dhcp server
73                    with.
74
75        Raises:
76            Error: Raised when a dhcp server error is found.
77        """
78        if self.is_alive():
79            self.stop()
80
81        self._write_configs(config)
82        self._shell.delete_file(self._stdio_log_file)
83        self._shell.delete_file(self._pid_file)
84        self._shell.touch_file(self._lease_file)
85
86        dhcpd_command = '%s -cf "%s" -lf %s -f -pf "%s"' % (
87            self.PROGRAM_FILE, self._config_file, self._lease_file,
88            self._pid_file)
89        base_command = 'cd "%s"; %s' % (self._working_dir, dhcpd_command)
90        job_str = '%s > "%s" 2>&1' % (base_command, self._stdio_log_file)
91        self._runner.run_async(job_str)
92
93        try:
94            self._wait_for_process(timeout=timeout)
95            self._wait_for_server(timeout=timeout)
96        except:
97            self._log.warn("Failed to start DHCP server.")
98            self._log.info("DHCP configuration:\n" +
99                           config.render_config_file() + "\n")
100            self._log.info("DHCP logs:\n" + self.get_logs() + "\n")
101            self.stop()
102            raise
103
104    def stop(self):
105        """Kills the daemon if it is running."""
106        if self.is_alive():
107            self._shell.kill(self._identifier)
108
109    def is_alive(self):
110        """
111        Returns:
112            True if the daemon is running.
113        """
114        return self._shell.is_alive(self._identifier)
115
116    def get_logs(self):
117        """Pulls the log files from where dhcp server is running.
118
119        Returns:
120            A string of the dhcp server logs.
121        """
122        try:
123            # Try reading the PID file. This will fail if the server failed to
124            # start.
125            pid = self._shell.read_file(self._pid_file)
126            # `dhcpd` logs to the syslog, where its messages are interspersed
127            # with all other programs that use the syslog. Log lines contain
128            # `dhcpd[<pid>]`, which we can search for to extract all the logs
129            # from this particular dhcpd instance.
130            # The logs are preferable to the stdio output, since they contain
131            # a superset of the information from stdio, including leases
132            # that the server provides.
133            return self._shell.run(
134                f"grep dhcpd.{pid} /var/log/messages").stdout
135        except Exception:
136            self._log.info(
137                "Failed to read logs from syslog (likely because the server " +
138                "failed to start). Falling back to stdio output.")
139            return self._shell.read_file(self._stdio_log_file)
140
141    def _wait_for_process(self, timeout=60):
142        """Waits for the process to come up.
143
144        Waits until the dhcp server process is found running, or there is
145        a timeout. If the program never comes up then the log file
146        will be scanned for errors.
147
148        Raises: See _scan_for_errors
149        """
150        start_time = time.time()
151        while time.time() - start_time < timeout and not self.is_alive():
152            self._scan_for_errors(False)
153            time.sleep(0.1)
154
155        self._scan_for_errors(True)
156
157    def _wait_for_server(self, timeout=60):
158        """Waits for dhcp server to report that the server is up.
159
160        Waits until dhcp server says the server has been brought up or an
161        error occurs.
162
163        Raises: see _scan_for_errors
164        """
165        start_time = time.time()
166        while time.time() - start_time < timeout:
167            success = self._shell.search_file(
168                'Wrote [0-9]* leases to leases file', self._stdio_log_file)
169            if success:
170                return
171
172            self._scan_for_errors(True)
173
174    def _scan_for_errors(self, should_be_up):
175        """Scans the dhcp server log for any errors.
176
177        Args:
178            should_be_up: If true then dhcp server is expected to be alive.
179                          If it is found not alive while this is true an error
180                          is thrown.
181
182        Raises:
183            Error: Raised when a dhcp server error is found.
184        """
185        # If this is checked last we can run into a race condition where while
186        # scanning the log the process has not died, but after scanning it
187        # has. If this were checked last in that condition then the wrong
188        # error will be thrown. To prevent this we gather the alive state first
189        # so that if it is dead it will definitely give the right error before
190        # just giving a generic one.
191        is_dead = not self.is_alive()
192
193        no_interface = self._shell.search_file(
194            'Not configured to listen on any interfaces', self._stdio_log_file)
195        if no_interface:
196            raise NoInterfaceError(
197                'Dhcp does not contain a subnet for any of the networks the'
198                ' current interfaces are on.')
199
200        if should_be_up and is_dead:
201            raise Error('Dhcp server failed to start.', self)
202
203    def _write_configs(self, config):
204        """Writes the configs to the dhcp server config file."""
205        self._shell.delete_file(self._config_file)
206        config_str = config.render_config_file()
207        self._shell.write_file(self._config_file, config_str)
208