blob: 1c3b65154b413d2c6905f4648ab24d0e1f6a162d [file] [log] [blame]
// 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
}