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