blob: edef28264d82bcd62dedd4c32a8425656c175820 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 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.
"""Provision a recovery image for OOBE autoconfiguration.
This script populates the OOBE autoconfiguration data
(/stateful/unencrypted/oobe_auto_config/config.json) with the given parameters.
Additionally, it marks the image as being "hands-free", i.e. requiring no
physical user interaction to remove the recovery media before rebooting after
the recovery procedure has completed.
Any parameters prefixed with --x (e.g. --x-demo-mode) correspond directly to
generated elements in the configuration expected by OOBE.
from __future__ import print_function
import json
import os
import sys
import uuid
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import image_lib
from chromite.lib import osutils
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
# OOBE auto-config parameters as they appear in
# chrome/browser/chromeos/login/configuration_keys.h
# Please keep the keys grouped in the same order as the source file.
('demo-mode', bool, 'Whether the device should be placed into demo mode.'),
('network-onc', str, 'ONC blob for network configuration.'),
('network-auto-connect', bool,
'Whether the network screen should automatically proceed with '
'connected network.'),
('eula-send-statistics', bool,
'Whether the device should send usage statistics.'),
('eula-auto-accept', bool,
'Whether the EULA should be automatically accepted.'),
('update-skip', bool,
'Whether the udpate check should be skipped entirely (it may be '
'required for future version pinning).'),
('wizard-auto-enroll', bool,
'Whether the wizard should automatically start enrollment at the '
'appropriate moment.'),
# Set of flags to specify when building with --generic.
'network-auto-connect': True,
'eula-send-statistics': True,
'eula-auto-accept': True,
'update-skip': True,
# Mapping of flag type to argparse kwargs.
str: {},
bool: {'action': 'store_true'},
# Name of the OOBE directory in unencrypted/.
_OOBE_DIRECTORY = 'oobe_auto_config'
# Name of the configuration file in the recovery image.
_CONFIG_PATH = 'config.json'
# Name of the file containing the enrollment domain.
_DOMAIN_PATH = 'enrollment_domain'
def SanitizeDomain(domain):
"""Sanitized |domain| for use in recovery.
domain: (str) The original string.
(str) The sanitized domain name, possibly using punycode to disambiguate.
# Encode using punycode ("idna" here) to prevent homograph attacks.
# Once that's been normalized to ASCII, normalize to lowercase.
return domain.encode('idna').decode('utf-8').lower()
def GetConfigContent(opts):
"""Formats OOBE autoconfiguration from commandline namespace.
opts: A commandline namespace containing OOBE autoconfig opts.
A JSON string representation of the requested configuration.
conf = {}
for flag, _, _ in _CONFIG_PARAMETERS:
conf[flag] = getattr(opts, 'x_' + flag.replace('-', '_'))
if opts.wifi_ssid:
conf['network-onc'] = {
'GUID': str(uuid.uuid4()),
'Name': opts.wifi_ssid,
'Type': 'WiFi',
'WiFi': {
'AutoConnect': True,
'HiddenSSID': False,
'SSID': opts.wifi_ssid,
'Security': 'None',
if opts.use_ethernet:
conf['network-onc'] = {
'GUID': str(uuid.uuid4()),
'Name': 'Ethernet',
'Type': 'Ethernet',
'Ethernet': {
'Authentication': 'None',
return json.dumps(conf)
def PrepareImage(path, content, domain=None):
"""Prepares a recovery image for OOBE autoconfiguration.
path: Path to the recovery image.
content: The content of the OOBE autoconfiguration.
domain: Which domain to enroll to.
with osutils.TempDir() as tmp, \
image_lib.LoopbackPartitions(path, tmp) as image:
stateful_mnt = image.Mount((constants.CROS_PART_STATEFUL,),
# /stateful/unencrypted may not exist at this point in time on the
# recovery image, so create it root-owned here.
unencrypted = os.path.join(stateful_mnt, 'unencrypted')
osutils.SafeMakedirs(unencrypted, mode=0o755, sudo=True)
# The OOBE autoconfig directory must be owned by the chronos user so
# that we can delete the config file from it from Chrome.
oobe_autoconf = os.path.join(unencrypted, _OOBE_DIRECTORY)
osutils.SafeMakedirsNonRoot(oobe_autoconf, user='chronos')
# Create the config file to be owned by the chronos user, and write the
# given data into it.
config = os.path.join(oobe_autoconf, _CONFIG_PATH)
osutils.WriteFile(config, content, sudo=True)
cros_build_lib.sudo_run(['chown', 'chronos:chronos', config])
# If we have a plaintext domain name, write it.
if domain:
domain_path = os.path.join(oobe_autoconf, _DOMAIN_PATH)
osutils.WriteFile(domain_path, SanitizeDomain(domain), sudo=True)
cros_build_lib.sudo_run(['chown', 'chronos:chronos', domain_path])
def ParseArguments(argv):
"""Returns a namespace for the CLI arguments."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument('image', help='Path of recovery image to populate.')
# Prefix raw config elements with --x.
for flag, flag_type, help_text in _CONFIG_PARAMETERS:
parser.add_argument('--x-%s' % flag, help=help_text,
parser.add_argument('--generic', action='store_true',
help='Set defaults for common configuration options.')
parser.add_argument('--dump-config', action='store_true',
help='Dump generated configuration file to stdout.')
parser.add_argument('--config', type='path', required=False,
help='Path to pre-generated configuration file to use, '
'overriding other flags set.')
parser.add_argument('--wifi-ssid', type=str, required=False,
help='If specified, generates an ONC for auto-connecting '
'to the given SSID. The network must not use any '
'security (i.e. be an open network), or the device '
'will fail to connect.')
parser.add_argument('--use-ethernet', action='store_true',
help='If specified, generates an ONC for auto-connecting '
'via ethernet.')
parser.add_argument('--enrollment-domain', type=str, required=False,
help='Text to visually identify the enrollment token in '
opts = parser.parse_args(argv)
if opts.use_ethernet and opts.wifi_ssid:
parser.error('cannot specify --wifi-ssid and --use-ethernet together')
if opts.generic:
for opt, val in _GENERIC_FLAGS.items():
setattr(opts, 'x_' + opt.replace('-', '_'), val)
return opts
def main(argv):
opts = ParseArguments(argv)
if opts.config:
config_content = osutils.ReadFile(opts.config)
config_content = GetConfigContent(opts)'Using config: %s', config_content)
if opts.dump_config:
PrepareImage(opts.image, config_content, opts.enrollment_domain)