| // Copyright 2019 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 devices |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/tools/net/serial" |
| |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| const ( |
| // The duration we allow for the netstack to come up when booting. |
| netstackTimeout = 90 * time.Second |
| ) |
| |
| type DeviceType string |
| |
| // DeviceConfig contains the static properties of a target device. |
| type DeviceConfig struct { |
| // Type tells us what kind of machine this is. |
| Type string `json:"type"` |
| |
| // Network is the network properties of the target. |
| Network NetworkProperties `json:"network"` |
| |
| // Power is the attached power management configuration. |
| Power *PowerClient `json:"power,omitempty"` |
| |
| // SSHKeys are the default system keys to be used with the device. |
| SSHKeys []string `json:"keys,omitempty"` |
| |
| // Serial is the path to the device file for serial i/o. |
| Serial string `json:"serial,omitempty"` |
| } |
| |
| // NetworkProperties are the static network properties of a target. |
| type NetworkProperties struct { |
| // Nodename is the hostname of the device that we want to boot on. |
| Nodename string `json:"nodename"` |
| |
| // IPv4Addr is the IPv4 address, if statically given. If not provided, it may be |
| // resolved via the netstack's MDNS server. |
| IPv4Addr string `json:"ipv4"` |
| |
| // Mac is the mac address of the device |
| Mac string `json:"mac"` |
| } |
| |
| // LoadDeviceConfigs unmarshalls a slice of device configs from a given file. |
| func LoadDeviceConfigs(path string) ([]DeviceConfig, error) { |
| data, err := ioutil.ReadFile(path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read device properties file %q", path) |
| } |
| |
| var configs []DeviceConfig |
| if err := json.Unmarshal(data, &configs); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal configs: %v", err) |
| } |
| return configs, nil |
| } |
| |
| // DeviceTarget represents a target device. |
| type DeviceTarget struct { |
| deviceType DeviceType |
| config DeviceConfig |
| signers []ssh.Signer |
| serial io.ReadWriteCloser |
| BootserverCmd []string |
| } |
| |
| // CreateDeviceTargets loads configs from the given file and creates DeviceTargets for each one |
| func CreateDeviceTargets(ctx context.Context, path string, bootserverCmdStub []string) ([]*DeviceTarget, error) { |
| deviceConfigs, err := LoadDeviceConfigs(path) |
| if err != nil { |
| return nil, err |
| } |
| var devices []*DeviceTarget |
| for _, deviceConfig := range deviceConfigs { |
| device, err := NewDeviceTarget(ctx, deviceConfig, bootserverCmdStub) |
| if err != nil { |
| return nil, err |
| } |
| devices = append(devices, device) |
| } |
| return devices, nil |
| } |
| |
| // NewDeviceTarget returns a new device target with a given configuration. |
| func NewDeviceTarget(ctx context.Context, config DeviceConfig, bootserverCmdStub []string) (*DeviceTarget, error) { |
| signers, err := parseOutSigners(config.SSHKeys) |
| if err != nil { |
| return nil, fmt.Errorf("could not parse out signers from private keys: %v", err) |
| } |
| var s io.ReadWriteCloser |
| if config.Serial != "" { |
| s, err = serial.Open(config.Serial) |
| if err != nil { |
| log.Printf("unable to open %s: %v", config.Serial, err) |
| } |
| } |
| deviceType, err := ConvertConfigType(config) |
| if err != nil { |
| return nil, err |
| } |
| return &DeviceTarget{ |
| deviceType: deviceType, |
| config: config, |
| signers: signers, |
| serial: s, |
| BootserverCmd: append(bootserverCmdStub, config.Network.Nodename), |
| }, nil |
| } |
| |
| // ConvertConfigType converts a device type from the configs into a more |
| // manageable representation. |
| func ConvertConfigType(config DeviceConfig) (DeviceType, error) { |
| switch config.Type { |
| case "Intel NUC Kit NUC7i5DNHE": |
| return "nuc", nil |
| case "Khadas Vim2 Max": |
| return "vim2", nil |
| case "Astro": |
| return "astro", nil |
| case "Sherlock": |
| return "sherlock", nil |
| case "Nelson": |
| return "nelson", nil |
| } |
| return "", fmt.Errorf("Invalid device type in configs: %s", config.Type) |
| } |
| |
| // SetConfig sets the config field of the given DeviceTarget |
| func (t *DeviceTarget) SetConfig(config DeviceConfig) { |
| t.config = config |
| } |
| |
| // Nodename returns the name of the node. |
| func (t *DeviceTarget) Nodename() string { |
| return t.config.Network.Nodename |
| } |
| |
| // IPv4Addr returns the IPv4 address of the node from the config |
| func (t *DeviceTarget) IPv4Addr() (net.IP, error) { |
| return net.ParseIP(t.config.Network.IPv4Addr), nil |
| } |
| |
| // Mac returns the Mac address of the node from the config |
| func (t *DeviceTarget) Mac() string { |
| return t.config.Network.Mac |
| } |
| |
| // Serial returns the serial device associated with the target for serial i/o. |
| func (t *DeviceTarget) Serial() io.ReadWriteCloser { |
| return t.serial |
| } |
| |
| // Type returns the type of the device. |
| func (t *DeviceTarget) Type() DeviceType { |
| return t.deviceType |
| } |
| |
| // Restart restarts the target. |
| func (t *DeviceTarget) Restart(ctx context.Context) error { |
| if t.serial != nil { |
| defer t.serial.Close() |
| } |
| if t.config.Power != nil { |
| if err := t.config.Power.RebootDevice(t.signers, t.Nodename(), t.serial); err != nil { |
| return fmt.Errorf("failed to reboot the device: %v", err) |
| } |
| } |
| return nil |
| } |
| |
| // Powercycle uses an out of band method to powercycle the target. |
| func (t *DeviceTarget) Powercycle(ctx context.Context) error { |
| if t.config.Power != nil { |
| return t.config.Power.Powercycle(t.Nodename(), t.Mac(), t.serial) |
| } |
| return nil |
| } |
| |
| func parseOutSigners(keyPaths []string) ([]ssh.Signer, error) { |
| if len(keyPaths) == 0 { |
| return nil, errors.New("must supply SSH keys in the config") |
| } |
| var keys [][]byte |
| for _, keyPath := range keyPaths { |
| p, err := ioutil.ReadFile(keyPath) |
| if err != nil { |
| return nil, fmt.Errorf("could not read SSH key file %q: %v", keyPath, err) |
| } |
| keys = append(keys, p) |
| } |
| |
| var signers []ssh.Signer |
| for _, p := range keys { |
| signer, err := ssh.ParsePrivateKey(p) |
| if err != nil { |
| return nil, err |
| } |
| signers = append(signers, signer) |
| } |
| return signers, nil |
| } |