blob: 74ccf266af1b52abd2da99c7f44cfb364adefda7 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package sdkcommon
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"go.fuchsia.dev/fuchsia/tools/lib/color"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
var (
// ExecCommand exports exec.Command as a variable so it can be mocked.
ExecCommand = exec.Command
// ExecLookPath exported to support mocking.
ExecLookPath = exec.LookPath
// logging support.
logLevel = logger.InfoLevel
log = logger.NewLogger(logLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "sdk ")
)
// Default GCS bucket for prebuilt images and packages.
const defaultGCSbucket string = "fuchsia"
// GCSImage is used to return the bucket, name and version of a prebuilt.
type GCSImage struct {
Bucket string
Name string
Version string
}
// SDKProperties holds the common data for SDK tools.
// These values should be set or initialized by calling
// (sdk *SDKProperties) Init().
type SDKProperties struct {
DataPath string
Version string
}
// DefaultGetUserHomeDir is the default implmentation of GetUserHomeDir()
// to allow mocking of user.Current()
func DefaultGetUserHomeDir() (string, error) {
usr, err := user.Current()
if err != nil {
return "", nil
}
return usr.HomeDir, nil
}
// DefaultGetUsername is the default implmentation of GetUsername()
// to allow mocking of user.Current()
func DefaultGetUsername() (string, error) {
usr, err := user.Current()
if err != nil {
return "", nil
}
return usr.Username, nil
}
// DefaultGetHostname is the default implmentation of GetHostname()
// to allow mocking of user.Current()
func DefaultGetHostname() (string, error) {
return os.Hostname()
}
// GetUserHomeDir allow mocking
var GetUserHomeDir = DefaultGetUserHomeDir
var GetUsername = DefaultGetUsername
var GetHostname = DefaultGetHostname
// Init initializes the SDK properties.
// TODO(fxb/64107): Refactor this to New()
func (sdk *SDKProperties) Init() error {
homeDir, err := GetUserHomeDir()
if err != nil {
return err
}
sdk.DataPath = filepath.Join(homeDir, ".fuchsia")
toolsDir, err := sdk.GetToolsDir()
if err != nil {
return err
}
manifestFile, err := filepath.Abs(filepath.Join(toolsDir, "..", "..", "meta", "manifest.json"))
if err != nil {
return err
}
// If this is running in-tree, the manifest may not exist.
if FileExists(manifestFile) {
if sdk.Version, err = getSDKVersion(manifestFile); err != nil {
return err
}
} else {
log.Warningf("Cannot find SDK manifest file %v", manifestFile)
}
return nil
}
// getSDKVersion reads the manifest JSON file and returns the "id" property.
func getSDKVersion(manifestFilePath string) (string, error) {
manifestFile, err := os.Open(manifestFilePath)
// if we os.Open returns an error then handle it
if err != nil {
return "", err
}
defer manifestFile.Close()
data, err := ioutil.ReadAll(manifestFile)
if err != nil {
return "", err
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(data), &result); err != nil {
return "", err
}
version, _ := result["id"].(string)
return version, nil
}
// GetDefaultPackageRepoDir returns the path to the package repository.
// This is the default package repository path, thinking there will
// be other repositories in the future.
func (sdk SDKProperties) GetDefaultPackageRepoDir() (string, error) {
return filepath.Join(sdk.DataPath, "packages", "amber-files"), nil
}
// GetDefaultGCSBucket returns the default GCS bucket name.
func (sdk SDKProperties) GetDefaultGCSBucket() (string, error) {
return "fuchsia", nil
}
// GetDefaultGCSImage returns the default GCS image name.
func (sdk SDKProperties) GetDefaultGCSImage() (string, error) {
return "", nil
}
// GetDefaultPackageServerPort returns the TCP port the package server should use.
func (sdk SDKProperties) GetDefaultPackageServerPort() (string, error) {
return "8083", nil
}
// GetDefaultDeviceName returns the default target device name.
func (sdk SDKProperties) GetDefaultDeviceName() (string, error) {
return "", nil
}
// GetDefaultDeviceIPAddress returns the default target device IP address.
func (sdk SDKProperties) GetDefaultDeviceIPAddress() (string, error) {
return "", nil
}
// GetToolsDir returns the path to the SDK tools for the current
// CPU architecture. This is implemented by default of getting the
// directory of the currently exeecuting binary.
func (sdk SDKProperties) GetToolsDir() (string, error) {
exePath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("Could not currently running file: %v", err)
}
dir, err := filepath.Abs(filepath.Dir(exePath))
if err != nil {
return "", fmt.Errorf("could not get directory of currently running file: %s", err)
}
return dir, nil
}
// GetAvailableImages returns the images available for the given version and bucket. If
// bucket is not the default bucket, the images in the default bucket are also returned.
func (sdk SDKProperties) GetAvailableImages(version string, bucket string) ([]GCSImage, error) {
var buckets []string
var images []GCSImage
if bucket == "" || bucket == defaultGCSbucket {
buckets = []string{defaultGCSbucket}
} else {
buckets = []string{bucket, defaultGCSbucket}
}
for _, b := range buckets {
url := fmt.Sprintf("gs://%v/development/%v/images", b, version)
args := []string{"ls", url}
output, err := runGSUtil(args)
if err != nil {
return images, err
}
for _, line := range strings.Split(strings.TrimSuffix(string(output), "\n"), "\n") {
if len(filepath.Base(line)) >= 4 {
name := filepath.Base(line)[:len(filepath.Base(line))-4]
images = append(images, GCSImage{Bucket: b, Version: version, Name: name})
} else {
log.Warningf("Could not parse image name: %v", line)
}
}
}
return images, nil
}
// GetPackageSourcePath returns the GCS path for the given values.
func (sdk SDKProperties) GetPackageSourcePath(version string, bucket string, image string) string {
return fmt.Sprintf("gs://%s/development/%s/packages/%s.tar.gz", bucket, version, image)
}
//GetAddressByName returns the IPv6 address of the device.
func (sdk SDKProperties) GetAddressByName(deviceName string) (string, error) {
toolsDir, err := sdk.GetToolsDir()
if err != nil {
return "", fmt.Errorf("Could not determine tools directory %v", err)
}
cmd := filepath.Join(toolsDir, "device-finder")
args := []string{"resolve", "-device-limit", "1", "-ipv4=false", deviceName}
output, err := ExecCommand(cmd, args...).Output()
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return "", fmt.Errorf("%v: %v", string(exitError.Stderr), exitError)
} else {
return "", err
}
}
return string(output), nil
}
// RunSSHCommand runs the command provided in args on the given target device.
// The customSSHconfig is optional and overrides the SSH configuration defined by the SDK.
// privateKey is optional to specify a private key to use to access the device.
func (sdk SDKProperties) RunSSHCommand(targetAddress string, customSSHConfig string, privateKey string, args []string) (string, error) {
if customSSHConfig == "" || privateKey == "" {
if err := checkSSHConfig(sdk); err != nil {
return "", err
}
}
var cmdArgs []string
if customSSHConfig != "" {
cmdArgs = append(cmdArgs, "-F", customSSHConfig)
} else {
cmdArgs = []string{"-F", getFuchsiaSSHConfigFile(sdk)}
}
if privateKey != "" {
cmdArgs = append(cmdArgs, "-i", privateKey)
}
cmdArgs = append(cmdArgs, targetAddress)
cmdArgs = append(cmdArgs, args...)
return runSSH(cmdArgs)
}
func getFuchsiaSSHConfigFile(sdk SDKProperties) string {
return filepath.Join(sdk.DataPath, "sshconfig")
}
/* This function creates the ssh keys needed to
work with devices running Fuchsia. There are two parts, the keys and the config.
There is a key for Fuchsia that is placed in a well-known location so that applications
which need to access the Fuchsia device can all use the same key. This is stored in
${HOME}/.ssh/fuchsia_ed25519.
The authorized key file used for paving is in ${HOME}/.ssh/fuchsia_authorized_keys.
The private key used when ssh'ing to the device is in ${HOME}/.ssh/fuchsia_ed25519.
The second part of is the sshconfig file used by the SDK when using SSH.
This is stored in the Fuchsia SDK data directory named sshconfig.
This script checks for the private key file being referenced in the sshconfig and
the matching version tag. If they are not present, the sshconfig file is regenerated.
*/
const sshConfigTag = "Fuchsia SDK config version 5 tag"
func checkSSHConfig(sdk SDKProperties) error {
// The ssh configuration should not be modified.
homeDir, err := GetUserHomeDir()
if err != nil {
return fmt.Errorf("SSH configuration requires a $HOME directory: %v", err)
}
userName, err := GetUsername()
if err != nil {
return fmt.Errorf("SSH configuration requires a user name: %v", err)
}
var (
sshDir = filepath.Join(homeDir, ".ssh")
authFile = filepath.Join(sshDir, "fuchsia_authorized_keys")
keyFile = filepath.Join(sshDir, "fuchsia_ed25519")
sshConfigFile = getFuchsiaSSHConfigFile(sdk)
)
// If the public and private key pair exist, and the sshconfig
// file is up to date, then our work here is done, return success.
if FileExists(authFile) && FileExists(keyFile) && FileExists(sshConfigFile) {
config, err := ioutil.ReadFile(sshConfigFile)
if err == nil {
if strings.Contains(string(config), sshConfigTag) {
return nil
}
}
// The version tag does not match, so remove the old config file.
os.Remove(sshConfigFile)
}
if err := os.MkdirAll(sshDir, 0755); err != nil {
return fmt.Errorf("Could not create %v: %v", sshDir, err)
}
// Check to migrate keys from old location
if !FileExists(authFile) || !FileExists(keyFile) {
if err := moveLegacyKeys(sdk, authFile, keyFile); err != nil {
return fmt.Errorf("Could not migrate legacy SSH keys: %v", err)
}
}
// Create keys if needed
if !FileExists(authFile) || !FileExists(keyFile) {
if !FileExists(keyFile) {
hostname, _ := GetHostname()
if hostname == "" {
hostname = "unknown"
}
if err := generateSSHKey(keyFile, userName, hostname); err != nil {
return fmt.Errorf("Could generate private SSH key: %v", err)
}
}
if err := generatePublicSSHKeyfile(keyFile, authFile); err != nil {
return fmt.Errorf("Could get public keys from private SSH key: %v", err)
}
}
if err := writeSSHConfigFile(sshConfigFile, sshConfigTag, keyFile); err != nil {
return fmt.Errorf("Could write sshconfig file %v: %v", sshConfigFile, err)
}
return nil
}
func generateSSHKey(keyFile string, username string, hostname string) error {
path, err := ExecLookPath("ssh-keygen")
if err != nil {
return fmt.Errorf("could not find ssh-keygen on path: %v", err)
}
args := []string{
"-P", "",
"-t", "ed25519",
"-f", keyFile,
"-C", fmt.Sprintf("%v@%v generated by Fuchsia GN SDK", username, hostname),
}
cmd := ExecCommand(path, args...)
_, err = cmd.Output()
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return fmt.Errorf("%v: %v", string(exitError.Stderr), exitError)
} else {
return err
}
}
return nil
}
func generatePublicSSHKeyfile(keyFile string, authFile string) error {
path, err := ExecLookPath("ssh-keygen")
if err != nil {
return fmt.Errorf("could not find ssh-keygen on path: %v", err)
}
args := []string{
"-y",
"-f", keyFile,
}
cmd := ExecCommand(path, args...)
publicKey, err := cmd.Output()
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return fmt.Errorf("%v: %v", string(exitError.Stderr), exitError)
} else {
return err
}
}
if err := os.MkdirAll(filepath.Dir(authFile), 0755); err != nil {
return err
}
output, err := os.Create(authFile)
if err != nil {
return err
}
defer output.Close()
fmt.Fprintln(output, publicKey)
return nil
}
func writeSSHConfigFile(sshConfigFile string, versionTag string, keyFile string) error {
if err := os.MkdirAll(filepath.Dir(sshConfigFile), 0755); err != nil {
return err
}
output, err := os.Create(sshConfigFile)
if err != nil {
return err
}
defer output.Close()
fmt.Fprintf(output, "# %s\n", versionTag)
fmt.Fprintf(output,
`# Configure port 8022 for connecting to a device with the local address.
# This makes it possible to forward 8022 to a device connected remotely.
# The fuchsia private key is used for the identity.
Host 127.0.0.1
Port 8022
Host ::1
Port 8022
Host *
# Turn off refusing to connect to hosts whose key has changed
StrictHostKeyChecking no
CheckHostIP no
# Disable recording the known hosts
UserKnownHostsFile=/dev/null
# Do not forward auth agent connection to remote, no X11
ForwardAgent no
ForwardX11 no
# Connection timeout in seconds
ConnectTimeout=10
# Check for server alive in seconds, max count before disconnecting
ServerAliveInterval 1
ServerAliveCountMax 10
# Try to keep the master connection open to speed reconnecting.
ControlMaster auto
ControlPersist yes
# When expanded, the ControlPath below cannot have more than 90 characters
# (total of 108 minus 18 used by a random suffix added by ssh).
# '%%C' expands to 40 chars and there are 9 fixed chars, so '~' can expand to
# up to 41 chars, which is a reasonable limit for a user's home in most
# situations. If '~' expands to more than 41 chars, the ssh connection
# will fail with an error like:
# unix_listener: path "..." too long for Unix domain socket
# A possible solution is to use /tmp instead of ~, but it has
# its own security concerns.
ControlPath=~/.ssh/fx-%%C
# Connect with user, use the identity specified.
User fuchsia
IdentitiesOnly yes
IdentityFile "%v"
GSSAPIDelegateCredentials no
`, keyFile)
return nil
}
func moveLegacyKeys(sdk SDKProperties, destAuthFile string, destKeyFile string) error {
// Check for legacy GN SDK key and copy it to the new location.
var (
legacySSHDir = filepath.Join(sdk.DataPath, ".ssh")
legacyKeyFile = filepath.Join(legacySSHDir, "pkey")
legacyAuthFile = filepath.Join(legacySSHDir, "authorized_keys")
)
if FileExists(legacyKeyFile) {
fmt.Fprintf(os.Stderr, "Migrating legacy key file %v to %v\n", legacyKeyFile, destKeyFile)
if err := os.Rename(legacyKeyFile, destKeyFile); err != nil {
return err
}
if FileExists(legacyAuthFile) {
if err := os.Rename(legacyAuthFile, destAuthFile); err != nil {
return err
}
}
}
return nil
}