blob: f2532ed238f7ac0cc3c894cb8cc2d77fb0f36a58 [file] [log] [blame]
#!/usr/bin/env python2
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utility to access ChromeOS Netboot firmware settings."""
from __future__ import print_function
import argparse
import pprint
import socket
import struct
import sys
try:
import fmap
except ImportError:
import factory_common # pylint:disable=unused-import
from cros.factory.gooftool import fmap
# Values to encode netboot settings.
CODE_TFTP_SERVER_IP = 1
CODE_KERNEL_ARGS = 2
CODE_BOOT_FILE = 3
CODE_ARGS_FILE = 4
SETTINGS_FMAP_SECTION = 'SHARED_DATA'
def _ParseArgs(argv):
"""Construct an ArgumentParser for this utility script."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--input', '-i', required=True,
help='Path to the firmware to modify; required')
parser.add_argument('--output', '-o',
help='Path to store output; if not specified we will '
'directly modify the input file')
parser.add_argument('--tftpserverip',
help='Set the TFTP server IP address (defaults to DHCP-'
'provided address)')
parser.add_argument('--bootfile',
help='Set the path of the TFTP boot file (defaults to '
'DHCP-provided file name)')
parser.add_argument('--argsfile',
help='Set the path of the TFTP file that provides the '
'kernel command line (overrides default and --arg)')
parser.add_argument('--board',
help='Set the cros_board to be passed into the kernel')
parser.add_argument('--omahaserver',
help='Set the Omaha server IP address')
parser.add_argument('--arg', '--kernel_arg', default=[], dest='kernel_args',
metavar='kernel_args', action='append',
help='Set extra kernel command line parameters (appended '
'to default string for factory)')
return parser.parse_args(argv)
class Image(object):
"""A class to represent a firmware image.
Areas in the image should be accessed using the [] operator which takes
the area name as its key.
Attributes:
data: The data in the entire image.
"""
def __init__(self, data):
"""Initialize an instance of Image
Args:
self: The instance of Image.
data: The data contianed within the image.
"""
try:
# FMAP identifier used by the cros_bundle_firmware family of utilities.
obj = fmap.fmap_decode(data, fmap_name='FMAP')
except struct.error:
# FMAP identifier used by coreboot's FMAP creation tools.
# The name signals that the FMAP covers the entire flash unlike, for
# example, the EC RW firmware's FMAP, which might also come as part of
# the image but covers a smaller section.
obj = fmap.fmap_decode(data, fmap_name='FLASH')
self.areas = {}
for area in obj['areas']:
self.areas[area['name']] = area
self.data = data
def __setitem__(self, key, value):
"""Write data into an area of the image.
If value is smaller than the area it's being written into, it will be
padded out with NUL bytes. If it's too big, a ValueError exception
will be raised.
Args:
self: The image instance.
key: The name of the area to overwrite.
value: The data to write into the area.
Raises:
ValueError: 'value' was too large to write into the selected area.
"""
area = self.areas[key]
if len(value) > area['size']:
raise ValueError('Too much data for FMAP area %s' % key)
value = value.ljust(area['size'], '\0')
self.data = (self.data[:area['offset']] + value +
self.data[area['offset'] + area['size']:])
def __getitem__(self, key):
"""Retrieve the data in an area of the image.
Args:
self: The image instance.
key: The area to retrieve.
Returns:
The data in that area of the image.
"""
area = self.areas[key]
return self.data[area['offset']:area['offset'] + area['size']]
class Settings(object):
"""A class which represents a collection of settings.
The settings can be updated after a firmware image has been built.
Attributes of this class other than the signature constant are stored in
the 'value' field of each attribute in the attributes dict.
Attributes:
signature: A constant which has a signature value at the front of the
settings when written into the image.
"""
signature = 'netboot\0'
class Attribute(object):
"""A class which represents a particular setting.
Attributes:
code: An enum value which identifies which setting this is.
value: The value the setting has been set to.
"""
def __init__(self, code, value):
"""Initialize an Attribute instance.
Args:
code: The code for this attribute.
value: The initial value of this attribute.
"""
self.code = code
self.value = value
@staticmethod
def padded_value(value):
value_len = len(value)
pad_len = ((value_len + 3) // 4) * 4 - value_len
return value + '\0' * pad_len
def pack(self):
"""Pack an attribute into a binary representation.
Args:
self: The Attribute to pack.
Returns:
The binary representation.
"""
if self.value:
value = self.value.pack()
else:
value = ''
value_len = len(value)
padded_value = self.padded_value(value)
format_str = '<II%ds' % len(padded_value)
return struct.pack(format_str, self.code, value_len, padded_value)
def __repr__(self):
return repr(self.value)
@classmethod
def unpack(cls, blob, offset=0):
"""Returns a pair of (decoded attribute, decoded length)."""
header_str = '<II'
header_len = struct.calcsize(header_str)
code, value_len = struct.unpack_from(header_str, blob, offset)
offset += header_len
value = blob[offset:offset + value_len]
offset += len(cls.padded_value(value))
if value:
setting = IpAddressValue if code == CODE_TFTP_SERVER_IP else StringValue
value = setting.unpack(value)
return cls(code, value), offset
def __init__(self, blob):
"""Initialize an instance of Settings.
Args:
self: The instance to initialize.
"""
# Decode blob if possible.
decoded = {}
if blob.startswith(self.signature):
offset = len(self.signature)
format_items = '<I'
items, = struct.unpack_from(format_items, blob, offset)
offset += struct.calcsize(format_items)
for unused_i in xrange(items):
new_attr, new_offset = self.Attribute.unpack(blob, offset)
offset = new_offset
decoded[new_attr.code] = new_attr
def GetAttribute(code):
return decoded.get(code, self.Attribute(code, None))
attributes = {
'tftp_server_ip': GetAttribute(CODE_TFTP_SERVER_IP),
'kernel_args': GetAttribute(CODE_KERNEL_ARGS),
'bootfile': GetAttribute(CODE_BOOT_FILE),
'argsfile': GetAttribute(CODE_ARGS_FILE),
}
self.__dict__['attributes'] = attributes
def __setitem__(self, name, value):
self.attributes[name].value = value
def __getattr__(self, name):
return self.attributes[name].value
def pack(self):
"""Pack a Settings object into a binary representation.
The packed binary representation can be put into an image.
Args:
self: The instance to pack.
Returns:
A binary representation of the settings.
"""
value = self.signature
value += struct.pack('<I', len(self.attributes))
for unused_i, attr in self.attributes.iteritems():
value += attr.pack()
return value
class StringValue(object):
"""Class for setting values that are stored simply as strings."""
def __init__(self, val):
"""Initialize an instance of StringValue.
Args:
self: The instance to initialize.
val: The value of the setting.
"""
self.val = val
def pack(self):
"""Pack the setting by returning its value as a string.
Args:
self: The instance to pack.
Returns:
The val field as a string.
"""
return str(self.val)
@classmethod
def unpack(cls, val):
return cls(val)
def __str__(self):
return str(self.val)
def __repr__(self):
return repr(self.val.strip('\0'))
class IpAddressValue(StringValue):
"""Class for IP address setting value."""
def __init__(self, val):
"""Initialize an IpAddressValue instance.
Args:
self: The instance to initialize.
val: A string representation of the IP address to be set to.
"""
in_addr = socket.inet_pton(socket.AF_INET, val)
super(IpAddressValue, self).__init__(in_addr)
@classmethod
def unpack(cls, val):
return cls(socket.inet_ntop(socket.AF_INET, val))
def __str__(self):
return socket.inet_ntop(socket.AF_INET, self.val)
def __repr__(self):
return repr(str(self))
def main(argv):
options = _ParseArgs(argv)
print('Reading from %s...' % options.input)
with open(options.input, 'r') as f:
image = Image(f.read())
settings = Settings(image[SETTINGS_FMAP_SECTION])
print('Current settings:')
pprint.pprint(settings.attributes)
if options.tftpserverip:
settings['tftp_server_ip'] = IpAddressValue(options.tftpserverip)
if options.bootfile:
settings['bootfile'] = StringValue(options.bootfile + '\0')
if options.argsfile:
settings['argsfile'] = StringValue(options.argsfile + '\0')
kernel_args = ''
if options.board:
kernel_args += 'cros_board=' + options.board + ' '
if options.omahaserver:
kernel_args += 'omahaserver=' + options.omahaserver + ' '
kernel_args += ' '.join(options.kernel_args)
kernel_args += '\0'
settings['kernel_args'] = StringValue(kernel_args)
new_blob = settings.pack()
output_name = options.output or options.input
# If output is specified with different name, always generate output.
do_output = output_name != options.input
if new_blob == image[SETTINGS_FMAP_SECTION][:len(new_blob)]:
print('Settings not changed.')
else:
print('Settings modified. New settings:')
pprint.pprint(settings.attributes)
image[SETTINGS_FMAP_SECTION] = new_blob
do_output = True
if do_output:
print('Generating output to %s...' % output_name)
with open(output_name, 'w') as f:
f.write(image.data)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))