blob: 7126b6e3ba9b86c1b081356ef6ecbb89f15cabf9 [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 (
"bufio"
"bytes"
"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 ")
)
// FuchsiaDevice represent a Fuchsia device.
type FuchsiaDevice struct {
// IPv4 or IPv6 of the Fuchsia device.
IpAddr string
// Nodename of the Fuchsia device.
Name string
}
// 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
}
// Property keys used to get and set device configuration
const (
DeviceNameKey string = "device-name"
BucketKey string = "bucket"
ImageKey string = "image"
DeviceIPKey string = "device-ip"
SSHPortKey string = "ssh-port"
PackageRepoKey string = "package-repo"
PackagePortKey string = "package-port"
DefaultKey string = "default"
// Top level key for storing data.
deviceConfigurationKey string = "DeviceConfiguration"
defaultDeviceKey string = "_DEFAULT_DEVICE_"
)
const (
defaultBucketName string = "fuchsia"
defaultSSHPort string = "22"
defaultPackagePort string = "8083"
)
var validPropertyNames = [...]string{
DeviceNameKey,
BucketKey,
ImageKey,
DeviceIPKey,
SSHPortKey,
PackageRepoKey,
PackagePortKey,
DefaultKey,
}
// DeviceConfig holds all the properties that are configured
// for a given devce.
type DeviceConfig struct {
DeviceName string `json:"device-name"`
Bucket string `json:"bucket"`
Image string `json:"image"`
DeviceIP string `json:"device-ip"`
SSHPort string `json:"ssh-port"`
PackageRepo string `json:"package-repo"`
PackagePort string `json:"package-port"`
IsDefault bool `json:"default"`
}
// 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
globalPropertiesFilename string
}
func (sdk SDKProperties) setDeviceDefaults(deviceConfig *DeviceConfig) DeviceConfig {
// no reasonable default for device-name
if deviceConfig.Bucket == "" {
deviceConfig.Bucket = defaultBucketName
}
// no reasonable default for image
// no reasonable default for device-ip
if deviceConfig.SSHPort == "" {
deviceConfig.SSHPort = defaultSSHPort
}
if deviceConfig.PackageRepo == "" {
deviceConfig.PackageRepo = sdk.getDefaultPackageRepoDir(deviceConfig.DeviceName)
}
if deviceConfig.PackagePort == "" {
deviceConfig.PackagePort = defaultPackagePort
}
return *deviceConfig
}
// Builds the data key for the given segments.
func getDeviceDataKey(segments []string) string {
var fullKey = []string{deviceConfigurationKey}
return strings.Join(append(fullKey, segments...), ".")
}
// 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 to allow mocking.
var GetUserHomeDir = DefaultGetUserHomeDir
// GetUsername to allow mocking.
var GetUsername = DefaultGetUsername
// GetHostname to allow mocking.
var GetHostname = DefaultGetHostname
// New creates an initialized SDKProperties
func New() (SDKProperties, error) {
sdk := SDKProperties{}
homeDir, err := GetUserHomeDir()
if err != nil {
return sdk, err
}
sdk.dataPath = filepath.Join(homeDir, ".fuchsia")
toolsDir, err := sdk.GetToolsDir()
if err != nil {
return sdk, err
}
manifestFile, err := filepath.Abs(filepath.Join(toolsDir, "..", "..", "meta", "manifest.json"))
if err != nil {
return sdk, err
}
// If this is running in-tree, the manifest may not exist.
if FileExists(manifestFile) {
if sdk.version, err = readSDKVersion(manifestFile); err != nil {
return sdk, err
}
}
sdk.globalPropertiesFilename = filepath.Join(sdk.dataPath, "global_ffx_props.json")
err = initFFXGlobalConfig(sdk)
return sdk, err
}
// GetSDKVersion returns the version of the SDK or empty if not set.
// Use sdkcommon.New() to create an initalized SDKProperties struct.
func (sdk SDKProperties) GetSDKVersion() string {
return sdk.version
}
// GetSDKDataPath returns the path to the directory for storing SDK related data,
// or empty if not set.
// Use sdkcommon.New() to create an initalized SDKProperties struct.
func (sdk SDKProperties) GetSDKDataPath() string {
return sdk.dataPath
}
// getSDKVersion reads the manifest JSON file and returns the "id" property.
func readSDKVersion(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.
// If the value has been set with `fconfig`, use that value.
// Otherwise if there is a default target defined, return the target
// specific path.
// Lastly, if there is nothing, return the default repo path.
func (sdk SDKProperties) getDefaultPackageRepoDir(deviceName string) string {
if deviceName != "" {
return filepath.Join(sdk.GetSDKDataPath(), deviceName,
"packages", "amber-files")
}
// As a last resort, `ffx` and the data are working as intended,
// but no default has been configured, so fall back to the generic
// legacy path.
return filepath.Join(sdk.GetSDKDataPath(), "packages", "amber-files")
}
// GetDefaultDeviceName returns the name of the target device to use by default.
func (sdk SDKProperties) GetDefaultDeviceName() (string, error) {
dataKey := getDeviceDataKey([]string{defaultDeviceKey})
data, err := getDeviceConfigurationData(sdk, dataKey)
if err != nil {
return "", err
}
if name, ok := data[dataKey].(string); ok {
return name, nil
} else if len(data) == 0 {
return "", nil
}
return "", fmt.Errorf("Cannot parse default device from %v", data)
}
// 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 {
bucketVersion := filepath.Base(filepath.Dir(filepath.Dir(line)))
name := filepath.Base(line)[:len(filepath.Base(line))-4]
images = append(images, GCSImage{Bucket: b, Version: bucketVersion, 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)
}
// RunFFXDoctor runs common checks for the ffx tool and host environment and returns
// the stdout.
func (sdk SDKProperties) RunFFXDoctor() (string, error) {
args := []string{"doctor"}
return sdk.RunFFX(args, false)
}
// GetAddressByName returns the IPv6 address of the device.
func (sdk SDKProperties) GetAddressByName(deviceName string) (string, error) {
// Uses ffx disovery workflow by default. The legacy device-finder
// workflow can be enabled by setting the environment variable FUCHSIA_DISABLED_ffx_discovery=1.
FUCHSIA_DISABLED_FFX_DISCOVERY := os.Getenv("FUCHSIA_DISABLED_ffx_discovery")
if FUCHSIA_DISABLED_FFX_DISCOVERY == "1" {
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 strings.TrimSpace(string(output)), nil
}
// TODO(fxb/69008): use ffx json output.
args := []string{"target", "list", "--format", "a", deviceName}
output, err := sdk.RunFFX(args, false)
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 strings.TrimSpace(output), nil
}
func (f *FuchsiaDevice) String() string {
return fmt.Sprintf("%s %s", f.IpAddr, f.Name)
}
// FindDeviceByName returns a fuchsia device matching a specific device name.
func (sdk SDKProperties) FindDeviceByName(deviceName string) (*FuchsiaDevice, error) {
devices, err := sdk.ListDevices()
if err != nil {
return nil, err
}
for _, device := range devices {
if device.Name == deviceName {
return device, nil
}
}
return nil, fmt.Errorf("no device with device name %s found", deviceName)
}
// FindDeviceByIP returns a fuchsia device matching a specific ip address.
func (sdk SDKProperties) FindDeviceByIP(ipAddr string) (*FuchsiaDevice, error) {
devices, err := sdk.ListDevices()
if err != nil {
return nil, err
}
for _, device := range devices {
if device.IpAddr == ipAddr {
return device, nil
}
}
return nil, fmt.Errorf("no device with IP address %s found", ipAddr)
}
// ListDevices returns all available fuchsia devices.
func (sdk SDKProperties) ListDevices() ([]*FuchsiaDevice, error) {
var devices []*FuchsiaDevice
var err error
var output string
// Uses ffx disovery workflow by default. The legacy device-finder
// workflow can be enabled by setting the environment variable FUCHSIA_DISABLED_ffx_discovery=1.
FUCHSIA_DISABLED_FFX_DISCOVERY := os.Getenv("FUCHSIA_DISABLED_ffx_discovery")
if FUCHSIA_DISABLED_FFX_DISCOVERY == "1" {
toolsDir, err := sdk.GetToolsDir()
if err != nil {
return nil, fmt.Errorf("Could not determine tools directory %v", err)
}
cmd := filepath.Join(toolsDir, "device-finder")
args := []string{"list", "--full", "-ipv4=false"}
outputAsBytes, err := ExecCommand(cmd, args...).Output()
if err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return nil, fmt.Errorf("%v: %v", string(exitError.Stderr), exitError)
}
return nil, err
}
output = string(outputAsBytes)
} else {
args := []string{"target", "list", "--format", "s"}
output, err = sdk.RunFFX(args, false)
if err != nil {
return nil, err
}
}
for _, line := range strings.Split(output, "\n") {
parts := strings.Split(line, " ")
if len(parts) == 2 {
devices = append(devices, &FuchsiaDevice{
IpAddr: strings.TrimSpace(parts[0]),
Name: strings.TrimSpace(parts[1]),
})
}
}
if len(devices) < 1 {
return nil, fmt.Errorf("no devices found")
}
return devices, nil
}
func getCommonSSHArgs(sdk SDKProperties, customSSHConfig string, privateKey string) []string {
var cmdArgs []string
if customSSHConfig != "" {
cmdArgs = append(cmdArgs, "-F", customSSHConfig)
} else {
cmdArgs = append(cmdArgs, "-F", getFuchsiaSSHConfigFile(sdk))
}
if privateKey != "" {
cmdArgs = append(cmdArgs, "-i", privateKey)
}
return cmdArgs
}
// RunSFTPCommand runs sftp (one of SSH's file copy tools).
// Setting to_target to true will copy file SRC from host to DST on the target.
// Otherwise it will copy file from SRC from target to DST on the host.
// The return value is the error if any.
func (sdk SDKProperties) RunSFTPCommand(targetAddress string, customSSHConfig string, privateKey string, to_target bool, src string, dst string) error {
commonArgs := []string{"-q", "-b", "-"}
if customSSHConfig == "" || privateKey == "" {
if err := checkSSHConfig(sdk); err != nil {
return err
}
}
cmdArgs := getCommonSSHArgs(sdk, customSSHConfig, privateKey)
cmdArgs = append(cmdArgs, commonArgs...)
if targetAddress == "" {
return errors.New("target address must be specified")
}
// SFTP needs the [] around the ipv6 address, which is different than ssh.
if strings.Contains(targetAddress, ":") {
targetAddress = fmt.Sprintf("[%v]", targetAddress)
}
cmdArgs = append(cmdArgs, targetAddress)
stdin := ""
if to_target {
stdin = fmt.Sprintf("put %v %v", src, dst)
} else {
stdin = fmt.Sprintf("get %v %v", src, dst)
}
return runSFTP(cmdArgs, stdin)
}
// 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.
// verbose adds the -v flag to ssh.
// The return value is the stdout.
func (sdk SDKProperties) RunSSHCommand(targetAddress string, customSSHConfig string, privateKey string, verbose bool, args []string) (string, error) {
cmdArgs, err := buildSSHArgs(sdk, targetAddress, customSSHConfig, privateKey, verbose, args)
if err != nil {
return "", err
}
return runSSH(cmdArgs, false)
}
// RunSSHShell runs the command provided in args on the given target device and
// uses the system stdin, stdout, stderr. Returns when the ssh process exits.
// 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.
// verbose adds the -v flag to ssh.
// The return value is the stdout.
func (sdk SDKProperties) RunSSHShell(targetAddress string, customSSHConfig string, privateKey string, verbose bool, args []string) error {
cmdArgs, err := buildSSHArgs(sdk, targetAddress, customSSHConfig, privateKey, verbose, args)
if err != nil {
return err
}
_, err = runSSH(cmdArgs, true)
return err
}
func buildSSHArgs(sdk SDKProperties, targetAddress string, customSSHConfig string,
privateKey string, verbose bool, args []string) ([]string, error) {
if customSSHConfig == "" || privateKey == "" {
if err := checkSSHConfig(sdk); err != nil {
return []string{}, err
}
}
cmdArgs := getCommonSSHArgs(sdk, customSSHConfig, privateKey)
if verbose {
cmdArgs = append(cmdArgs, "-v")
}
if targetAddress == "" {
return cmdArgs, errors.New("target address must be specified")
}
cmdArgs = append(cmdArgs, targetAddress)
cmdArgs = append(cmdArgs, args...)
return cmdArgs, nil
}
func getFuchsiaSSHConfigFile(sdk SDKProperties) string {
return filepath.Join(sdk.GetSDKDataPath(), "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.GetSDKDataPath(), ".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
}
// GetValidPropertyNames returns the list of valid properties for a
// device configuration.
func (sdk SDKProperties) GetValidPropertyNames() []string {
return validPropertyNames[:]
}
// IsValidProperty returns true if the property is a valid
// property name.
func (sdk SDKProperties) IsValidProperty(property string) bool {
for _, item := range validPropertyNames {
if item == property {
return true
}
}
return false
}
// GetFuchsiaProperty returns the value for the given property for the given device.
// If the device name is empty, the default device is used via GetDefaultDeviceName().
// It is an error if the property cannot be found.
func (sdk SDKProperties) GetFuchsiaProperty(device string, property string) (string, error) {
var err error
deviceName := device
if deviceName == "" {
if deviceName, err = sdk.GetDefaultDeviceName(); err != nil {
return "", err
}
}
deviceConfig, err := sdk.GetDeviceConfiguration(deviceName)
if err != nil {
return "", fmt.Errorf("Could not read configuration data for %v : %v", deviceName, err)
}
switch property {
case BucketKey:
return deviceConfig.Bucket, nil
case DeviceIPKey:
return deviceConfig.DeviceIP, nil
case DeviceNameKey:
return deviceConfig.DeviceName, nil
case ImageKey:
return deviceConfig.Image, nil
case PackagePortKey:
return deviceConfig.PackagePort, nil
case PackageRepoKey:
return deviceConfig.PackageRepo, nil
case SSHPortKey:
return deviceConfig.SSHPort, nil
}
return "", fmt.Errorf("Could not find property %v.%v", deviceName, property)
}
// GetDeviceConfigurations returns a list of all device configurations.
func (sdk SDKProperties) GetDeviceConfigurations() ([]DeviceConfig, error) {
var configs []DeviceConfig
// Get all config data.
configData, err := getDeviceConfigurationData(sdk, deviceConfigurationKey)
if err != nil {
return configs, fmt.Errorf("Could not read configuration data : %v", err)
}
if len(configData) == 0 {
return configs, nil
}
defaultDeviceName, err := sdk.GetDefaultDeviceName()
if err != nil {
return configs, err
}
if deviceConfigMap, ok := configData[deviceConfigurationKey].(map[string]interface{}); ok {
for k, v := range deviceConfigMap {
if !isReservedProperty(k) {
if device, ok := mapToDeviceConfig(v); ok {
device.IsDefault = defaultDeviceName == device.DeviceName
configs = append(configs, device)
}
}
}
return configs, nil
}
return configs, fmt.Errorf("Could not read configuration data: %v", configData)
}
// GetDeviceConfiguration returns the configuration for the device with the given name.
func (sdk SDKProperties) GetDeviceConfiguration(name string) (DeviceConfig, error) {
var deviceConfig DeviceConfig
dataKey := getDeviceDataKey([]string{name})
configData, err := getDeviceConfigurationData(sdk, dataKey)
if err != nil {
return deviceConfig, fmt.Errorf("Could not read configuration data : %v", err)
}
if len(configData) == 0 {
sdk.setDeviceDefaults(&deviceConfig)
return deviceConfig, nil
}
if deviceData, ok := configData[dataKey]; ok {
if deviceConfig, ok := mapToDeviceConfig(deviceData); ok {
defaultDeviceName, err := sdk.GetDefaultDeviceName()
if err != nil {
return deviceConfig, err
}
deviceConfig.IsDefault = deviceConfig.DeviceName == defaultDeviceName
// Set the default values for the device, even if not set explicitly
// This centralizes the configuration into 1 place.
sdk.setDeviceDefaults(&deviceConfig)
return deviceConfig, nil
}
return deviceConfig, fmt.Errorf("Cannot parse DeviceConfig from %v", configData)
}
return deviceConfig, fmt.Errorf("Cannot parse DeviceData.%v from %v", name, configData)
}
// SaveDeviceConfiguration persists the given device configuration properties.
func (sdk SDKProperties) SaveDeviceConfiguration(newConfig DeviceConfig) error {
// Create a map of key to value to store. Only write out values that are explicitly set to something
// that is not the default.
origConfig, err := sdk.GetDeviceConfiguration(newConfig.DeviceName)
if err != nil {
return err
}
var defaultConfig = DeviceConfig{}
sdk.setDeviceDefaults(&defaultConfig)
dataMap := make(map[string]string)
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, DeviceNameKey})] = newConfig.DeviceName
// if the value changed from the orginal, write it out.
if origConfig.Bucket != newConfig.Bucket {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, BucketKey})] = newConfig.Bucket
} else if defaultConfig.Bucket == newConfig.Bucket {
// if the new value is the default value, then write the empty string.
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, BucketKey})] = ""
}
if origConfig.DeviceIP != newConfig.DeviceIP {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, DeviceIPKey})] = newConfig.DeviceIP
} else if defaultConfig.DeviceIP == newConfig.DeviceIP {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, DeviceIPKey})] = ""
}
if origConfig.Image != newConfig.Image {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, ImageKey})] = newConfig.Image
} else if defaultConfig.Image == newConfig.Image {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, ImageKey})] = ""
}
if origConfig.PackagePort != newConfig.PackagePort {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, PackagePortKey})] = newConfig.PackagePort
} else if defaultConfig.PackagePort == newConfig.PackagePort {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, PackagePortKey})] = ""
}
if origConfig.PackageRepo != newConfig.PackageRepo {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, PackageRepoKey})] = newConfig.PackageRepo
} else if defaultConfig.PackageRepo == newConfig.PackageRepo {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, PackageRepoKey})] = ""
}
if origConfig.SSHPort != newConfig.SSHPort {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, SSHPortKey})] = newConfig.SSHPort
} else if defaultConfig.SSHPort == newConfig.SSHPort {
dataMap[getDeviceDataKey([]string{newConfig.DeviceName, SSHPortKey})] = ""
}
if newConfig.IsDefault {
dataMap[getDeviceDataKey([]string{defaultDeviceKey})] = newConfig.DeviceName
}
for key, value := range dataMap {
err := writeConfigurationData(sdk, key, value)
if err != nil {
return err
}
}
return nil
}
// RemoveDeviceConfiguration removes the device settings for the given name.
func (sdk SDKProperties) RemoveDeviceConfiguration(deviceName string) error {
dataKey := getDeviceDataKey([]string{deviceName})
args := []string{"config", "remove", "--level", "global", dataKey}
if _, err := sdk.RunFFX(args, false); err != nil {
return fmt.Errorf("Error removing %s configuration: %v", deviceName, err)
}
defaultDeviceName, err := sdk.GetDefaultDeviceName()
if err != nil {
return err
}
if defaultDeviceName == deviceName {
err := writeConfigurationData(sdk, getDeviceDataKey([]string{defaultDeviceKey}), "")
if err != nil {
return err
}
}
return nil
}
// ResolveTargetAddress evaulates the deviceIP and deviceName passed in
// to determine the target IP address. This include consulting the configuration
// information set via `fconfig`.
func (sdk SDKProperties) ResolveTargetAddress(deviceIP string, deviceName string) (string, error) {
var (
targetAddress string
err error
)
helpfulTipMsg := `Try running "ffx target list --format s" and then "fconfig set-device <device_name> --image <image_name> --default".`
// If there is a deviceIP address, use it.
if deviceIP != "" {
targetAddress = deviceIP
} else {
// No explicit address, use the name
if deviceName == "" {
// No name passed in, use the default name.
if deviceName, err = sdk.GetDefaultDeviceName(); err != nil {
return "", fmt.Errorf("could not determine default device name.\n%v %v", helpfulTipMsg, err)
}
}
if deviceName == "" {
// No address specified, no device name specified, and no device configured as the default.
return "", fmt.Errorf("invalid arguments. Need to specify --device-ip or --device-name or use fconfig to configure a default device.\n%v", helpfulTipMsg)
}
// look up a configured address by devicename
targetAddress, err = sdk.GetFuchsiaProperty(deviceName, DeviceIPKey)
if err != nil {
return "", fmt.Errorf("could not read configuration information for %v.\n%v %v", deviceName, helpfulTipMsg, err)
}
// if still nothing, resolve the device address by name
if targetAddress == "" {
if targetAddress, err = sdk.GetAddressByName(deviceName); err != nil {
return "", fmt.Errorf(`cannot get target address for %v.
Try running "ffx target list --format s" and verify the name matches in "fconfig get-all". %v`, deviceName, err)
}
}
}
if targetAddress == "" {
return "", fmt.Errorf(`could not get target device IP address for %v.
Try running "ffx target list --format s" and verify the name matches in "fconfig get-all".`, deviceName)
}
return targetAddress, nil
}
func initFFXGlobalConfig(sdk SDKProperties) error {
args := []string{"config", "env"}
var (
err error
output string
line string
)
if output, err = sdk.RunFFX(args, false); err != nil {
return fmt.Errorf("Error getting config environment %v", err)
}
reader := bufio.NewReader(bytes.NewReader([]byte(output)))
hasGlobal := false
for !hasGlobal {
line, err = reader.ReadString('\n')
if err != nil {
if err.Error() == "EOF" {
break
} else {
return err
}
}
if strings.HasPrefix(strings.TrimSpace(line), "Global") {
break
}
}
doSetEnv := len(line) == 0
if len(line) > 0 {
const (
prefix = "Global:"
prefixLen = len(prefix)
)
index := strings.Index(line, "Global:")
if index > len(line) {
return fmt.Errorf("Cannot parse `Global:` prefix from %v", line)
}
filename := strings.TrimSpace(line[index+prefixLen:])
_, err := os.Stat(filename)
doSetEnv = os.IsNotExist(err)
}
if doSetEnv {
// Create the global config level
if len(sdk.globalPropertiesFilename) == 0 {
return fmt.Errorf("Cannot initialize property config, global file name is empty: %v", sdk)
}
args := []string{"config", "env", "set", sdk.globalPropertiesFilename, "--level", "global"}
if _, err := sdk.RunFFX(args, false); err != nil {
return fmt.Errorf("Error initializing global properties environment: %v", err)
}
}
return nil
}
// writeConfigurationData calls `ffx` to store the value at the specified key.
func writeConfigurationData(sdk SDKProperties, key string, value string) error {
args := []string{"config", "set", "--level", "global", key, value}
if output, err := sdk.RunFFX(args, false); err != nil {
return fmt.Errorf("Error writing %v = %v: %v %v", key, value, err, output)
}
return nil
}
// getDeviceConfigurationData calls `ffx` to read the data at the specified key.
func getDeviceConfigurationData(sdk SDKProperties, key string) (map[string]interface{}, error) {
var (
data map[string]interface{}
err error
output string
)
args := []string{"config", "get", key}
if output, err = sdk.RunFFX(args, false); err != nil {
// Exit code of 2 means no value was found.
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 2 {
return data, nil
}
return data, fmt.Errorf("Error reading %v: %v %v", key, err, output)
}
if len(output) > 0 {
jsonString := string(output)
// wrap the response in {} and double quote the key so it is suitable for json unmarshaling.
fullJSONString := "{\"" + key + "\": " + jsonString + "}"
err := json.Unmarshal([]byte(fullJSONString), &data)
if err != nil {
return data, fmt.Errorf("Error parsing configuration data %v: %s", err, fullJSONString)
}
}
return data, nil
}
// RunFFX executes ffx with the given args, returning stdout. If there is an error,
// the error will usually be of type *ExitError.
func (sdk SDKProperties) RunFFX(args []string, interactive bool) (string, error) {
toolsDir, err := sdk.GetToolsDir()
if err != nil {
return "", fmt.Errorf("Could not determine tools directory %v", err)
}
cmd := filepath.Join(toolsDir, "ffx")
ffx := ExecCommand(cmd, args...)
if interactive {
ffx.Stderr = os.Stderr
ffx.Stdout = os.Stdout
ffx.Stdin = os.Stdin
return "", ffx.Run()
}
output, err := ffx.Output()
if err != nil {
return "", err
}
return string(output), err
}
// isReservedProperty used to differenciate between properties used
// internally and device names.
func isReservedProperty(property string) bool {
switch property {
case defaultDeviceKey:
return true
}
return false
}
// mapToDeviceConfig converts the map returned by json into a DeviceConfig struct.
func mapToDeviceConfig(data interface{}) (DeviceConfig, bool) {
var (
device DeviceConfig
deviceData map[string]interface{}
ok bool
value string
)
if deviceData, ok = data.(map[string]interface{}); ok {
for _, key := range validPropertyNames {
// the Default flag is stored else where, so don't try to
// key it from the map.
if key == DefaultKey {
continue
}
if val, ok := deviceData[key].(string); ok {
value = val
} else {
fmt.Fprintf(os.Stderr, "Cannot get %v from %v", key, deviceData)
continue
}
switch key {
case BucketKey:
device.Bucket = value
case DeviceIPKey:
device.DeviceIP = value
case DeviceNameKey:
device.DeviceName = value
case ImageKey:
device.Image = value
case PackagePortKey:
device.PackagePort = value
case PackageRepoKey:
device.PackageRepo = value
case SSHPortKey:
device.SSHPort = value
}
}
}
return device, ok
}