autotest: Change DHCP Field and Option to namedtuple

This lets us use these objects as fields in a dictionary without
implementing a lot of very tedious operation methods.  This in turn
fixes this nonsense where we use the names of Options in some places and
the Option numbers in others.

BUG=chromium-os:34417
TEST=unittests pass, autotest tests pass

Change-Id: I971b98505d0a1e95fd40e6ea3557a7dee63dfa62
Reviewed-on: https://gerrit.chromium.org/gerrit/33151
Commit-Ready: Christopher Wiley <[email protected]>
Reviewed-by: Christopher Wiley <[email protected]>
Tested-by: Christopher Wiley <[email protected]>
diff --git a/client/cros/dhcp_packet.py b/client/cros/dhcp_packet.py
index 87f54d3..3d55389 100644
--- a/client/cros/dhcp_packet.py
+++ b/client/cros/dhcp_packet.py
@@ -29,94 +29,66 @@
 file still pass.
 """
 
+import collections
 import logging
 import random
 import socket
 import struct
 
-class Option(object):
-    """
-    Represents an option in a DHCP packet.  Options may or may not be present
-    and are not parsed into any particular format.  This means that the value of
-    options is always in the form of a byte string.
-    """
-    def __init__(self, name, number, size):
-        super(Option, self).__init__()
-        self._name = name
-        self._number = number
-        self._size = size
+"""
+Represents an option in a DHCP packet.  Options may or may not be present
+and are not parsed into any particular format.  This means that the value of
+options is always in the form of a byte string.
 
-    @property
-    def name(self):
-        return self._name
+|name|
+A human readable name for this option.
 
-    @property
-    def number(self):
-        """
-        Every DHCP option has a number that goes into the packet to indicate
-        which particular option is being encoded in the next few bytes.  This
-        property returns that number for each option.
-        """
-        return self._number
+|number|
+Every DHCP option has a number that goes into the packet to indicate
+which particular option is being encoded in the next few bytes.  This
+property returns that number for each option.
 
-    @property
-    def size(self):
-        """
-        The size property is a hint for what kind of size we might expect the
-        option to be.  For instance, options with a size of 1 are expected to
-        always be 1 byte long.  Negative sizes are variable length fields that
-        are expected to be at least abs(size) bytes long.
+|size|
+The size property is a hint for what kind of size we might expect the
+option to be.  For instance, options with a size of 1 are expected to
+always be 1 byte long.  Negative sizes are variable length fields that
+are expected to be at least abs(size) bytes long.
 
-        However, the size property is just a hint, and is not enforced or
-        checked in any way.
-        """
-        return self._size
+However, the size property is just a hint, and is not enforced or
+checked in any way.
+"""
+Option = collections.namedtuple("Option", ["name", "number", "size"])
 
+"""
+Represents a required field in a DHCP packet.  Unlike options, we sometimes
+parse fields into more meaningful data types.  For instance, the hardware
+type field in an IPv4 packet is parsed into an int rather than being left as
+a raw byte string of length 1.
 
-class Field(object):
-    """
-    Represents a required field in a DHCP packet.  Unlike options, we sometimes
-    parse fields into more meaningful data types.  For instance, the hardware
-    type field in an IPv4 packet is parsed into an int rather than being left as
-    a raw byte string of length 1.
-    """
-    def __init__(self, name, wire_format, offset, size):
-        super(Field, self).__init__()
-        self._name = name
-        self._wire_format = wire_format
-        self._offset = offset
-        self._size = size
+|name|
+A human readable name for this option.
 
-    @property
-    def name(self):
-        return self._name
+|wire_format|
+The wire format for a field defines how it will be parsed out of a DHCP
+packet.  For instance, the value for |FIELD_OP| is an integer in Python land,
+but goes on the wire as "!B", a single (network order) byte.  Fields that
+contain IP addresses like FIELD_SERVER_IP are strings of octets (like
+"\x0A\x0A\x01\x01" as returned by socket.inet_aton("10.10.1.1")) in Python land,
+and "!4s" on the wire, which is just a network order byte string, exactly like
+the Python format.
 
-    @property
-    def wire_format(self):
-        """
-        The wire format for a field defines how it will be parsed out of a DHCP
-        packet.
-        """
-        return self._wire_format
+|offset|
+The |offset| for a field defines the starting byte of the field in the
+binary packet string.  |offset| is used during parsing, along with
+|size| to extract the byte string of a field.
 
-    @property
-    def offset(self):
-        """
-        The |offset| for a field defines the starting byte of the field in the
-        binary packet string.  |offset| is using during parsing, along with
-        |size| to extract the byte string of a field.
-        """
-        return self._offset
-
-    @property
-    def size(self):
-        """
-        Fields in DHCP packets have a fixed size that must be respected.  This
-        size property is used in parsing to indicate that |self._size| number of
-        bytes make up this field.
-        """
-        return self._size
-
+|size|
+Fields in DHCP packets have a fixed size that must be respected.  This
+size property is used in parsing to indicate that |self._size| number of
+bytes make up this field.
+"""
+Field = collections.namedtuple("Field",
+                               ["name", "wire_format", "offset", "size"])
 
 # This is per RFC 2131.  The wording doesn't seem to say that the packets must
 # be this big, but that has been the historic assumption in implementations.
@@ -219,6 +191,7 @@
 OPTION_VALUE_DHCP_MESSAGE_TYPE_NAK       = "\x06"
 OPTION_VALUE_DHCP_MESSAGE_TYPE_RELEASE   = "\x07"
 OPTION_VALUE_DHCP_MESSAGE_TYPE_INFORM    = "\x08"
+OPTION_VALUE_DHCP_MESSAGE_TYPE_UNKNOWN   = "\xFF"
 
 OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = \
         chr(OPTION_SUBNET_MASK.number) + \
@@ -305,22 +278,22 @@
             hwmac_addr += chr(OPTION_PAD)
 
         packet = DhcpPacket()
-        packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_CLIENT_REQUEST)
-        packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
-        packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
-        packet.set_field(FIELD_RELAY_HOPS.name, 0)
-        packet.set_field(FIELD_TRANSACTION_ID.name, random.getrandbits(32))
-        packet.set_field(FIELD_TIME_SINCE_START.name, 0)
-        packet.set_field(FIELD_FLAGS.name, 0)
-        packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_YOUR_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_SERVER_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
-        packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
-        packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
+        packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST)
+        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
+        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
+        packet.set_field(FIELD_RELAY_HOPS, 0)
+        packet.set_field(FIELD_TRANSACTION_ID, random.getrandbits(32))
+        packet.set_field(FIELD_TIME_SINCE_START, 0)
+        packet.set_field(FIELD_FLAGS, 0)
+        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
+        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
+        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
                           OPTION_VALUE_DHCP_MESSAGE_TYPE_DISCOVERY)
-        packet.set_option(OPTION_PARAMETER_REQUEST_LIST.name,
+        packet.set_option(OPTION_PARAMETER_REQUEST_LIST,
                           OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT)
 
         return packet
@@ -337,26 +310,26 @@
         particular offer.
         """
         packet = DhcpPacket()
-        packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_SERVER_RESPONSE)
-        packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
-        packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
+        packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE)
+        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
+        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
         # This has something to do with relay agents
-        packet.set_field(FIELD_RELAY_HOPS.name, 0)
-        packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
-        packet.set_field(FIELD_TIME_SINCE_START.name, 0)
-        packet.set_field(FIELD_FLAGS.name, 0)
-        packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_YOUR_IP.name, socket.inet_aton(offer_ip))
-        packet.set_field(FIELD_SERVER_IP.name, socket.inet_aton(server_ip))
-        packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
-        packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
-        packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
+        packet.set_field(FIELD_RELAY_HOPS, 0)
+        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
+        packet.set_field(FIELD_TIME_SINCE_START, 0)
+        packet.set_field(FIELD_FLAGS, 0)
+        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_YOUR_IP, socket.inet_aton(offer_ip))
+        packet.set_field(FIELD_SERVER_IP, socket.inet_aton(server_ip))
+        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
+        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
+        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
                           OPTION_VALUE_DHCP_MESSAGE_TYPE_OFFER)
-        packet.set_option(OPTION_SUBNET_MASK.name,
+        packet.set_option(OPTION_SUBNET_MASK,
                           socket.inet_aton(offer_subnet_mask))
-        packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
-        packet.set_option(OPTION_IP_LEASE_TIME.name,
+        packet.set_option(OPTION_SERVER_ID, socket.inet_aton(server_ip))
+        packet.set_option(OPTION_IP_LEASE_TIME,
                           struct.pack("!I", int(lease_time_seconds)))
         return packet
 
@@ -366,26 +339,26 @@
                               requested_ip,
                               server_ip):
         packet = DhcpPacket()
-        packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_CLIENT_REQUEST)
-        packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
-        packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
+        packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST)
+        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
+        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
         # This has something to do with relay agents
-        packet.set_field(FIELD_RELAY_HOPS.name, 0)
-        packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
-        packet.set_field(FIELD_TIME_SINCE_START.name, 0)
-        packet.set_field(FIELD_FLAGS.name, 0)
-        packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_YOUR_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_SERVER_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
-        packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
-        packet.set_option(OPTION_REQUESTED_IP.name,
+        packet.set_field(FIELD_RELAY_HOPS, 0)
+        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
+        packet.set_field(FIELD_TIME_SINCE_START, 0)
+        packet.set_field(FIELD_FLAGS, 0)
+        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
+        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
+        packet.set_option(OPTION_REQUESTED_IP,
                           socket.inet_aton(requested_ip))
-        packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
+        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
                           OPTION_VALUE_DHCP_MESSAGE_TYPE_REQUEST)
-        packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
-        packet.set_option(OPTION_PARAMETER_REQUEST_LIST.name,
+        packet.set_option(OPTION_SERVER_ID, socket.inet_aton(server_ip))
+        packet.set_option(OPTION_PARAMETER_REQUEST_LIST,
                           OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT)
         return packet
 
@@ -397,26 +370,26 @@
                                       server_ip,
                                       lease_time_seconds):
         packet = DhcpPacket()
-        packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_SERVER_RESPONSE)
-        packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
-        packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
+        packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE)
+        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
+        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
         # This has something to do with relay agents
-        packet.set_field(FIELD_RELAY_HOPS.name, 0)
-        packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
-        packet.set_field(FIELD_TIME_SINCE_START.name, 0)
-        packet.set_field(FIELD_FLAGS.name, 0)
-        packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_YOUR_IP.name, socket.inet_aton(granted_ip))
-        packet.set_field(FIELD_SERVER_IP.name, socket.inet_aton(server_ip))
-        packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
-        packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
-        packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
-        packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
+        packet.set_field(FIELD_RELAY_HOPS, 0)
+        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
+        packet.set_field(FIELD_TIME_SINCE_START, 0)
+        packet.set_field(FIELD_FLAGS, 0)
+        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_YOUR_IP, socket.inet_aton(granted_ip))
+        packet.set_field(FIELD_SERVER_IP, socket.inet_aton(server_ip))
+        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
+        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
+        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
+        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
                           OPTION_VALUE_DHCP_MESSAGE_TYPE_ACK)
-        packet.set_option(OPTION_SUBNET_MASK.name,
+        packet.set_option(OPTION_SUBNET_MASK,
                           socket.inet_aton(granted_ip_subnet_mask))
-        packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
-        packet.set_option(OPTION_IP_LEASE_TIME.name,
+        packet.set_option(OPTION_SERVER_ID, socket.inet_aton(server_ip))
+        packet.set_option(OPTION_IP_LEASE_TIME,
                           struct.pack("!I", int(lease_time_seconds)))
         return packet
 
@@ -448,10 +421,10 @@
             self._logger.error("Invalid byte string for packet.")
             return
         for field in DHCP_PACKET_FIELDS:
-            self._fields[field.name] = struct.unpack(field.wire_format,
-                                                     byte_str[field.offset :
-                                                              field.offset +
-                                                              field.size])[0]
+            self._fields[field] = struct.unpack(field.wire_format,
+                                                byte_str[field.offset :
+                                                         field.offset +
+                                                         field.size])[0]
         offset = OPTIONS_START_OFFSET
         while offset < len(byte_str) and ord(byte_str[offset]) != OPTION_END:
             data_type = ord(byte_str[offset])
@@ -466,49 +439,49 @@
             if option_bunch is None:
                 # Unsupported data type, of which we have many.
                 continue
-            self._options[option_bunch.name] = data
+            if option_bunch == OPTION_PARAMETER_REQUEST_LIST:
+                options = [ord(c) for c in data]
+                logging.info("Requested options: %s" % str(options))
+            self._options[option_bunch] = data
 
     @property
     def client_hw_address(self):
-        return self._fields["chaddr"]
+        return self._fields.get(FIELD_CLIENT_HWADDR)
 
     @property
     def is_valid(self):
+        """
+        Checks that we have (at a minimum) values for all the fields, and that
+        the magic cookie is set correctly.
+        """
         for field in DHCP_PACKET_FIELDS:
-            if (not field.name in self._fields or
-                self._fields[field.name] is None):
-                self._logger.info("Missing field %s in packet." % field.name)
+            if self._fields.get(field) is None:
+                self._logger.info("Missing field %s in packet." % field)
                 return False
-        if (self._fields[FIELD_MAGIC_COOKIE.name] !=
-            FIELD_VALUE_MAGIC_COOKIE):
+        if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE:
             return False
         return True
 
     @property
     def message_type(self):
-        if not "dhcp_message_type" in self._options:
-            return -1
-        return self._options["dhcp_message_type"]
+        return self._options.get(OPTION_DHCP_MESSAGE_TYPE,
+                                 OPTION_VALUE_DHCP_MESSAGE_TYPE_UNKNOWN)
 
     @property
     def transaction_id(self):
-        return self._fields["xid"]
+        return self._fields.get(FIELD_TRANSACTION_ID)
 
-    def get_field(self, field_name):
-        if field_name in self._fields:
-            return self._fields[field_name]
-        return None
+    def get_field(self, field):
+        return self._fields.get(field)
 
-    def get_option(self, option_name):
-        if option_name in self._options:
-            return self._options[option_name]
-        return None
+    def get_option(self, option):
+        return self._options.get(option)
 
-    def set_field(self, field_name, field_value):
-        self._fields[field_name] = field_value
+    def set_field(self, field, field_value):
+        self._fields[field] = field_value
 
-    def set_option(self, option_name, option_value):
-        self._options[option_name] = option_value
+    def set_option(self, option, option_value):
+        self._options[option] = option_value
 
     def to_binary_string(self):
         if not self.is_valid:
@@ -518,7 +491,7 @@
         offset = 0
         for field in DHCP_PACKET_FIELDS:
             field_data = struct.pack(field.wire_format,
-                                     self._fields[field.name])
+                                     self._fields[field])
             while offset < field.offset:
                 # This should only happen when we're padding the fields because
                 # we're not filling in legacy BOOTP stuff.
@@ -529,17 +502,23 @@
         # Last field processed is the magic cookie, so we're ready for options.
         # Have to process options
         for option in DHCP_PACKET_OPTIONS:
-            if not option.name in self._options:
+            option_value = self._options.get(option)
+            if option_value is None:
                 continue
             data.append(struct.pack("BB",
                                     option.number,
-                                    len(self._options[option.name])))
+                                    len(option_value)))
             offset += 2
-            data.append(self._options[option.name])
-            offset += len(self._options[option.name])
+            data.append(option_value)
+            offset += len(option_value)
         data.append(chr(OPTION_END))
         offset += 1
         while offset < DHCP_MIN_PACKET_SIZE:
             data.append(chr(OPTION_PAD))
             offset += 1
         return "".join(data)
+
+    def __str__(self):
+        options = [k.name + "=" + str(v) for k, v in self._options.items()]
+        fields = [k.name + "=" + str(v) for k, v in self._fields.items()]
+        return "<DhcpPacket fields=%s, options=%s>" % (fields, options)