blob: 086b82cd1bdf8df5e618e8f985355e850b6af420 [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions used to provision Fuchsia boot images."""
import common
import logging
import os
import subprocess
import tempfile
import time
import uuid
_SSH_CONFIG_TEMPLATE = """
Host *
CheckHostIP no
StrictHostKeyChecking no
ForwardAgent no
ForwardX11 no
UserKnownHostsFile {known_hosts}
User fuchsia
IdentitiesOnly yes
IdentityFile {identity}
ServerAliveInterval 2
ServerAliveCountMax 5
ControlMaster auto
ControlPersist 1m
ControlPath /tmp/ssh-%r@%h:%p"""
FVM_TYPE_QCOW = 'qcow'
FVM_TYPE_SPARSE = 'sparse'
def _TargetCpuToSdkBinPath(target_arch):
"""Returns the path to the SDK 'target' file directory for |target_cpu|."""
return os.path.join(common.SDK_ROOT, 'target', target_arch)
def _ProvisionSSH(output_dir):
"""Provisions the key files used by the SSH daemon, and generates a
configuration file used by clients for connecting to SSH.
Returns a tuple with:
#0: the client configuration file
#1: a list of file path pairs: (<path in image>, <path on build filesystem>).
"""
host_key_path = output_dir + '/ssh_key'
host_pubkey_path = host_key_path + '.pub'
id_key_path = output_dir + '/id_ed25519'
id_pubkey_path = id_key_path + '.pub'
known_hosts_path = output_dir + '/known_hosts'
ssh_config_path = GetSSHConfigPath(output_dir)
logging.debug('Generating SSH credentials.')
if not os.path.isfile(host_key_path):
subprocess.check_call(['ssh-keygen', '-t', 'ed25519', '-h', '-f',
host_key_path, '-P', '', '-N', ''],
stdout=open(os.devnull))
if not os.path.isfile(id_key_path):
subprocess.check_call(['ssh-keygen', '-t', 'ed25519', '-f', id_key_path,
'-P', '', '-N', ''], stdout=open(os.devnull))
with open(ssh_config_path, "w") as ssh_config:
ssh_config.write(
_SSH_CONFIG_TEMPLATE.format(identity=id_key_path,
known_hosts=known_hosts_path))
if os.path.exists(known_hosts_path):
os.remove(known_hosts_path)
return (
ssh_config_path,
(('ssh/ssh_host_ed25519_key', host_key_path),
('ssh/ssh_host_ed25519_key.pub', host_pubkey_path),
('ssh/authorized_keys', id_pubkey_path))
)
def _MakeQcowDisk(output_dir, disk_path):
"""Creates a QEMU copy-on-write version of |disk_path| in the output
directory."""
qimg_path = os.path.join(common.SDK_ROOT, 'qemu', 'bin', 'qemu-img')
output_path = os.path.join(output_dir,
os.path.basename(disk_path) + '.qcow2')
subprocess.check_call([qimg_path, 'create', '-q', '-f', 'qcow2',
'-b', disk_path, output_path])
return output_path
def GetTargetFile(target_arch, filename):
"""Computes a path to |filename| in the Fuchsia target directory specific to
|target_arch|."""
return os.path.join(_TargetCpuToSdkBinPath(target_arch), filename)
def GetSSHConfigPath(output_dir):
return output_dir + '/ssh_config'
def ConfigureDataFVM(output_dir, output_type):
"""Builds the FVM image for the /data volume and prepopulates it
with SSH keys.
output_dir: Path to the output directory which will contain the FVM file.
output_type: If FVM_TYPE_QCOW, then returns a path to the qcow2 FVM file,
used for QEMU.
If FVM_TYPE_SPARSE, then returns a path to the
sparse/compressed FVM file."""
logging.debug('Building /data partition FVM file.')
# minfs expects absolute paths(bug:
# https://fuchsia.atlassian.net/browse/ZX-2397)
output_dir = os.path.abspath(output_dir)
with tempfile.NamedTemporaryFile() as data_file:
# Build up the minfs partition data and install keys into it.
ssh_config, ssh_data = _ProvisionSSH(output_dir)
with tempfile.NamedTemporaryFile() as manifest:
for dest, src in ssh_data:
manifest.write('%s=%s\n' % (dest, src))
manifest.flush()
minfs_path = os.path.join(common.SDK_ROOT, 'tools', 'minfs')
subprocess.check_call([minfs_path, '%s@1G' % data_file.name, 'create'])
subprocess.check_call([minfs_path, data_file.name, 'manifest',
manifest.name])
# Wrap the minfs partition in a FVM container.
fvm_path = os.path.join(common.SDK_ROOT, 'tools', 'fvm')
fvm_output_path = os.path.join(output_dir, 'fvm.data.blk')
if os.path.exists(fvm_output_path):
os.remove(fvm_output_path)
if output_type == FVM_TYPE_SPARSE:
cmd = [fvm_path, fvm_output_path, 'sparse', '--compress', 'lz4',
'--data', data_file.name]
else:
cmd = [fvm_path, fvm_output_path, 'create', '--data', data_file.name]
logging.debug(' '.join(cmd))
subprocess.check_call(cmd)
if output_type == FVM_TYPE_SPARSE:
return fvm_output_path
elif output_type == FVM_TYPE_QCOW:
return _MakeQcowDisk(output_dir, fvm_output_path)
else:
raise Exception('Unknown output_type: %r' % output_type)
def GetNodeName(output_dir):
"""Returns the cached Zircon node name, or generates one if it doesn't
already exist. The node name is used by Discover to find the prior
deployment on the LAN."""
nodename_file = os.path.join(output_dir, 'nodename')
if not os.path.exists(nodename_file):
nodename = uuid.uuid4()
f = open(nodename_file, 'w')
f.write(str(nodename))
f.flush()
f.close()
return str(nodename)
else:
f = open(nodename_file, 'r')
return f.readline()
def GetKernelArgs(output_dir):
return ['devmgr.epoch=%d' % time.time(),
'zircon.nodename=' + GetNodeName(output_dir)]