Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 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 
      5 """
      6 Programmable testing DHCP server.
      7 
      8 Simple DHCP server you can program with expectations of future packets and
      9 responses to those packets.  The server is basically a thin wrapper around a
     10 server socket with some utility logic to make setting up tests easier.  To write
     11 a test, you start a server, construct a sequence of handling rules.
     12 
     13 Handling rules let you set up expectations of future packets of certain types.
     14 Handling rules are processed in order, and only the first remaining handler
     15 handles a given packet.  In theory you could write the entire test into a single
     16 handling rule and keep an internal state machine for how far that handler has
     17 gotten through the test.  This would be poor style however.  Correct style is to
     18 write (or reuse) a handler for each packet the server should see, leading us to
     19 a happy land where any conceivable packet handler has already been written for
     20 us.
     21 
     22 Example usage:
     23 
     24 # Start up the DHCP server, which will ignore packets until a test is started
     25 server = DhcpTestServer(interface="veth_master")
     26 server.start()
     27 
     28 # Given a list of handling rules, start a test with a 30 sec timeout.
     29 handling_rules = []
     30 handling_rules.append(DhcpHandlingRule_RespondToDiscovery(intended_ip,
     31                                                           intended_subnet_mask,
     32                                                           dhcp_server_ip,
     33                                                           lease_time_seconds)
     34 server.start_test(handling_rules, 30.0)
     35 
     36 # Trigger DHCP clients to do various test related actions
     37 ...
     38 
     39 # Get results
     40 server.wait_for_test_to_finish()
     41 if (server.last_test_passed):
     42     ...
     43 else:
     44     ...
     45 
     46 
     47 Note that if you make changes, make sure that the tests in dhcp_unittest.py
     48 still pass.
     49 """
     50 
     51 import logging
     52 import socket
     53 import threading
     54 import time
     55 import traceback
     56 
     57 from autotest_lib.client.cros import dhcp_packet
     58 from autotest_lib.client.cros import dhcp_handling_rule
     59 
     60 # From socket.h
     61 SO_BINDTODEVICE = 25
     62 
     63 class DhcpTestServer(threading.Thread):
     64     def __init__(self,
     65                  interface=None,
     66                  ingress_address="<broadcast>",
     67                  ingress_port=67,
     68                  broadcast_address="255.255.255.255",
     69                  broadcast_port=68):
     70         super(DhcpTestServer, self).__init__()
     71         self._mutex = threading.Lock()
     72         self._ingress_address = ingress_address
     73         self._ingress_port = ingress_port
     74         self._broadcast_port = broadcast_port
     75         self._broadcast_address = broadcast_address
     76         self._socket = None
     77         self._interface = interface
     78         self._stopped = False
     79         self._test_in_progress = False
     80         self._last_test_passed = False
     81         self._test_timeout = 0
     82         self._handling_rules = []
     83         self._logger = logging.getLogger("dhcp.test_server")
     84         self._exception = None
     85         self.daemon = False
     86 
     87     @property
     88     def stopped(self):
     89         with self._mutex:
     90             return self._stopped
     91 
     92     @property
     93     def is_healthy(self):
     94         with self._mutex:
     95             return self._socket is not None
     96 
     97     @property
     98     def test_in_progress(self):
     99         with self._mutex:
    100             return self._test_in_progress
    101 
    102     @property
    103     def last_test_passed(self):
    104         with self._mutex:
    105             return self._last_test_passed
    106 
    107     @property
    108     def current_rule(self):
    109         """
    110         Return the currently active DhcpHandlingRule.
    111         """
    112         with self._mutex:
    113             return self._handling_rules[0]
    114 
    115     def start(self):
    116         """
    117         Start the DHCP server.  Only call this once.
    118         """
    119         if self.is_alive():
    120             return False
    121         self._logger.info("DhcpTestServer started; opening sockets.")
    122         try:
    123             self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    124             self._logger.info("Opening socket on '%s' port %d." %
    125                               (self._ingress_address, self._ingress_port))
    126             self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    127             self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    128             if self._interface is not None:
    129                 self._logger.info("Binding to %s" % self._interface)
    130                 self._socket.setsockopt(socket.SOL_SOCKET,
    131                                         SO_BINDTODEVICE,
    132                                         self._interface)
    133             self._socket.bind((self._ingress_address, self._ingress_port))
    134             # Wait 100 ms for a packet, then return, thus keeping the thread
    135             # active but mostly idle.
    136             self._socket.settimeout(0.1)
    137         except socket.error, socket_error:
    138             self._logger.error("Socket error: %s." % str(socket_error))
    139             self._logger.error(traceback.format_exc())
    140             if not self._socket is None:
    141                 self._socket.close()
    142             self._socket = None
    143             self._logger.error("Failed to open server socket.  Aborting.")
    144             return
    145         super(DhcpTestServer, self).start()
    146 
    147     def stop(self):
    148         """
    149         Stop the DHCP server and free its socket.
    150         """
    151         with self._mutex:
    152             self._stopped = True
    153 
    154     def start_test(self, handling_rules, test_timeout_seconds):
    155         """
    156         Start a new test using |handling_rules|.  The server will call the
    157         test successfull if it receives a RESPONSE_IGNORE_SUCCESS (or
    158         RESPONSE_RESPOND_SUCCESS) from a handling_rule before
    159         |test_timeout_seconds| passes.  If the timeout passes without that
    160         message, the server runs out of handling rules, or a handling rule
    161         return RESPONSE_FAIL, the test is ended and marked as not passed.
    162 
    163         All packets received before start_test() is called are received and
    164         ignored.
    165         """
    166         with self._mutex:
    167             self._test_timeout = time.time() + test_timeout_seconds
    168             self._handling_rules = handling_rules
    169             self._test_in_progress = True
    170             self._last_test_passed = False
    171             self._exception = None
    172 
    173     def wait_for_test_to_finish(self):
    174         """
    175         Block on the test finishing in a CPU friendly way.  Timeouts, successes,
    176         and failures count as finishes.
    177         """
    178         while self.test_in_progress:
    179             time.sleep(0.1)
    180         if self._exception:
    181             raise self._exception
    182 
    183     def abort_test(self):
    184         """
    185         Abort a test prematurely, counting the test as a failure.
    186         """
    187         with self._mutex:
    188             self._logger.info("Manually aborting test.")
    189             self._end_test_unsafe(False)
    190 
    191     def _teardown(self):
    192         with self._mutex:
    193             self._socket.close()
    194             self._socket = None
    195 
    196     def _end_test_unsafe(self, passed):
    197         if not self._test_in_progress:
    198             return
    199         if passed:
    200             self._logger.info("DHCP server says test passed.")
    201         else:
    202             self._logger.info("DHCP server says test failed.")
    203         self._test_in_progress = False
    204         self._last_test_passed = passed
    205 
    206     def _send_response_unsafe(self, packet):
    207         if packet is None:
    208             self._logger.error("Handling rule failed to return a packet.")
    209             return False
    210         self._logger.debug("Sending response: %s" % packet)
    211         binary_string = packet.to_binary_string()
    212         if binary_string is None or len(binary_string) < 1:
    213             self._logger.error("Packet failed to serialize to binary string.")
    214             return False
    215 
    216         self._socket.sendto(binary_string,
    217                             (self._broadcast_address, self._broadcast_port))
    218         return True
    219 
    220     def _loop_body(self):
    221         with self._mutex:
    222             if self._test_in_progress and self._test_timeout < time.time():
    223                 # The test has timed out, so we abort it.  However, we should
    224                 # continue to accept packets, so we fall through.
    225                 self._logger.error("Test in progress has timed out.")
    226                 self._end_test_unsafe(False)
    227             try:
    228                 data, _ = self._socket.recvfrom(1024)
    229                 self._logger.info("Server received packet of length %d." %
    230                                    len(data))
    231             except socket.timeout:
    232                 # No packets available, lets return and see if the server has
    233                 # been shut down in the meantime.
    234                 return
    235 
    236             # Receive packets when no test is in progress, just don't process
    237             # them.
    238             if not self._test_in_progress:
    239                 return
    240 
    241             packet = dhcp_packet.DhcpPacket(byte_str=data)
    242             if not packet.is_valid:
    243                 self._logger.warning("Server received an invalid packet over a "
    244                                      "DHCP port?")
    245                 return
    246 
    247             logging.debug("Server received a DHCP packet: %s." % packet)
    248             if len(self._handling_rules) < 1:
    249                 self._logger.info("No handling rule for packet: %s." %
    250                                   str(packet))
    251                 self._end_test_unsafe(False)
    252                 return
    253 
    254             handling_rule = self._handling_rules[0]
    255             response_code = handling_rule.handle(packet)
    256             logging.info("Handler gave response: %d" % response_code)
    257             if response_code & dhcp_handling_rule.RESPONSE_POP_HANDLER:
    258                 self._handling_rules.pop(0)
    259 
    260             if response_code & dhcp_handling_rule.RESPONSE_HAVE_RESPONSE:
    261                 for response_instance in range(
    262                         handling_rule.response_packet_count):
    263                     response = handling_rule.respond(packet)
    264                     if not self._send_response_unsafe(response):
    265                         self._logger.error(
    266                                 "Failed to send packet, ending test.")
    267                         self._end_test_unsafe(False)
    268                         return
    269 
    270             if response_code & dhcp_handling_rule.RESPONSE_TEST_FAILED:
    271                 self._logger.info("Handling rule %s rejected packet %s." %
    272                                   (handling_rule, packet))
    273                 self._end_test_unsafe(False)
    274                 return
    275 
    276             if response_code & dhcp_handling_rule.RESPONSE_TEST_SUCCEEDED:
    277                 self._end_test_unsafe(True)
    278                 return
    279 
    280     def run(self):
    281         """
    282         Main method of the thread.  Never call this directly, since it assumes
    283         some setup done in start().
    284         """
    285         with self._mutex:
    286             if self._socket is None:
    287                 self._logger.error("Failed to create server socket, exiting.")
    288                 return
    289 
    290         self._logger.info("DhcpTestServer entering handling loop.")
    291         while not self.stopped:
    292             try:
    293                 self._loop_body()
    294                 # Python does not have waiting queues on Lock objects.
    295                 # Give other threads a change to hold the mutex by
    296                 # forcibly releasing the GIL while we sleep.
    297                 time.sleep(0.01)
    298             except Exception as e:
    299                 with self._mutex:
    300                     self._end_test_unsafe(False)
    301                     self._exception = e
    302         with self._mutex:
    303             self._end_test_unsafe(False)
    304         self._logger.info("DhcpTestServer closing sockets.")
    305         self._teardown()
    306         self._logger.info("DhcpTestServer exiting.")
    307