| # 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 1 | 
 |   ServerAliveCountMax 1 | 
 |   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.') | 
 |   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)] |