• 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 acts.controllers.utils_lib.commands import shell
17
18_ROUTER_DNS = '8.8.8.8, 4.4.4.4'
19
20
21class Error(Exception):
22    """An error caused by the dhcp server."""
23
24
25class NoInterfaceError(Exception):
26    """Error thrown when the dhcp server has no interfaces on any subnet."""
27
28
29class DhcpServer(object):
30    """Manages the dhcp server program.
31
32    Only one of these can run in an environment at a time.
33
34    Attributes:
35        config: The dhcp server configuration that is being used.
36    """
37
38    PROGRAM_FILE = 'dhcpd'
39
40    def __init__(self, runner, interface, working_dir='/tmp'):
41        """
42        Args:
43            runner: Object that has a run_async and run methods for running
44                    shell commands.
45            interface: string, The name of the interface to use.
46            working_dir: The directory to work out of.
47        """
48        self._runner = runner
49        self._working_dir = working_dir
50        self._shell = shell.ShellCommand(runner, working_dir)
51        self._log_file = 'dhcpd_%s.log' % interface
52        self._config_file = 'dhcpd_%s.conf' % interface
53        self._lease_file = 'dhcpd_%s.leases' % interface
54        self._identifier = '%s.*%s' % (self.PROGRAM_FILE, self._config_file)
55
56    def start(self, config, timeout=60):
57        """Starts the dhcp server.
58
59        Starts the dhcp server daemon and runs it in the background.
60
61        Args:
62            config: dhcp_config.DhcpConfig, Configs to start the dhcp server
63                    with.
64
65        Returns:
66            True if the daemon could be started. Note that the daemon can still
67            start and not work. Invalid configurations can take a long amount
68            of time to be produced, and because the daemon runs indefinitely
69            it's infeasible to wait on. If you need to check if configs are ok
70            then periodic checks to is_running and logs should be used.
71        """
72        if self.is_alive():
73            self.stop()
74
75        self._write_configs(config)
76        self._shell.delete_file(self._log_file)
77        self._shell.touch_file(self._lease_file)
78
79        dhcpd_command = '%s -cf "%s" -lf %s -f""' % (self.PROGRAM_FILE,
80                                                     self._config_file,
81                                                     self._lease_file)
82        base_command = 'cd "%s"; %s' % (self._working_dir, dhcpd_command)
83        job_str = '%s > "%s" 2>&1' % (base_command, self._log_file)
84        self._runner.run_async(job_str)
85
86        try:
87            self._wait_for_process(timeout=timeout)
88            self._wait_for_server(timeout=timeout)
89        except:
90            self.stop()
91            raise
92
93    def stop(self):
94        """Kills the daemon if it is running."""
95        self._shell.kill(self._identifier)
96
97    def is_alive(self):
98        """
99        Returns:
100            True if the daemon is running.
101        """
102        return self._shell.is_alive(self._identifier)
103
104    def get_logs(self):
105        """Pulls the log files from where dhcp server is running.
106
107        Returns:
108            A string of the dhcp server logs.
109        """
110        return self._shell.read_file(self._log_file)
111
112    def _wait_for_process(self, timeout=60):
113        """Waits for the process to come up.
114
115        Waits until the dhcp server process is found running, or there is
116        a timeout. If the program never comes up then the log file
117        will be scanned for errors.
118
119        Raises: See _scan_for_errors
120        """
121        start_time = time.time()
122        while time.time() - start_time < timeout and not self.is_alive():
123            self._scan_for_errors(False)
124            time.sleep(0.1)
125
126        self._scan_for_errors(True)
127
128    def _wait_for_server(self, timeout=60):
129        """Waits for dhcp server to report that the server is up.
130
131        Waits until dhcp server says the server has been brought up or an
132        error occurs.
133
134        Raises: see _scan_for_errors
135        """
136        start_time = time.time()
137        while time.time() - start_time < timeout:
138            success = self._shell.search_file(
139                'Wrote [0-9]* leases to leases file', self._log_file)
140            if success:
141                return
142
143            self._scan_for_errors(True)
144
145    def _scan_for_errors(self, should_be_up):
146        """Scans the dhcp server log for any errors.
147
148        Args:
149            should_be_up: If true then dhcp server is expected to be alive.
150                          If it is found not alive while this is true an error
151                          is thrown.
152
153        Raises:
154            Error: Raised when a dhcp server error is found.
155        """
156        # If this is checked last we can run into a race condition where while
157        # scanning the log the process has not died, but after scanning it
158        # has. If this were checked last in that condition then the wrong
159        # error will be thrown. To prevent this we gather the alive state first
160        # so that if it is dead it will definitely give the right error before
161        # just giving a generic one.
162        is_dead = not self.is_alive()
163
164        no_interface = self._shell.search_file(
165            'Not configured to listen on any interfaces', self._log_file)
166        if no_interface:
167            raise NoInterfaceError(
168                'Dhcp does not contain a subnet for any of the networks the'
169                ' current interfaces are on.')
170
171        if should_be_up and is_dead:
172            raise Error('Dhcp server failed to start.', self)
173
174    def _write_configs(self, config):
175        """Writes the configs to the dhcp server config file."""
176
177        self._shell.delete_file(self._config_file)
178
179        lines = []
180
181        if config.default_lease_time:
182            lines.append('default-lease-time %d;' % config.default_lease_time)
183        if config.max_lease_time:
184            lines.append('max-lease-time %s;' % config.max_lease_time)
185
186        for subnet in config.subnets:
187            address = subnet.network.network_address
188            mask = subnet.network.netmask
189            router = subnet.router
190            start = subnet.start
191            end = subnet.end
192            lease_time = subnet.lease_time
193
194            lines.append('subnet %s netmask %s {' % (address, mask))
195            lines.append('\toption subnet-mask %s;' % mask)
196            lines.append('\toption routers %s;' % router)
197            lines.append('\toption domain-name-servers %s;' % _ROUTER_DNS)
198            lines.append('\trange %s %s;' % (start, end))
199            if lease_time:
200                lines.append('\tdefault-lease-time %d;' % lease_time)
201                lines.append('\tmax-lease-time %d;' % lease_time)
202            lines.append('}')
203
204        for mapping in config.static_mappings:
205            identifier = mapping.identifier
206            fixed_address = mapping.ipv4_address
207            host_fake_name = 'host%s' % identifier.replace(':', '')
208            lease_time = mapping.lease_time
209
210            lines.append('host %s {' % host_fake_name)
211            lines.append('\thardware ethernet %s;' % identifier)
212            lines.append('\tfixed-address %s;' % fixed_address)
213            if lease_time:
214                lines.append('\tdefault-lease-time %d;' % lease_time)
215                lines.append('\tmax-lease-time %d;' % lease_time)
216            lines.append('}')
217
218        config_str = '\n'.join(lines)
219
220        self._shell.write_file(self._config_file, config_str)
221