blob: 1380d0bd733cf3b9b0d769fa86fdf731e82b47fc [file] [log] [blame]
// Copyright 2018 The Chromium OS 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 vm
import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"strings"
"github.com/godbus/dbus"
"github.com/golang/protobuf/proto"
cpb "chromiumos/system_api/vm_cicerone_proto" // protobufs for container management
vmpb "chromiumos/system_api/vm_concierge_proto" // protobufs for VM management
"chromiumos/tast/errors"
"chromiumos/tast/local/cryptohome"
"chromiumos/tast/local/dbusutil"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
const (
conciergeJob = "vm_concierge" // name of the upstart job for concierge
ciceroneJob = "vm_cicerone" // name of the upstart job for cicerone
conciergeName = "org.chromium.VmConcierge"
conciergePath = dbus.ObjectPath("/org/chromium/VmConcierge")
conciergeInterface = "org.chromium.VmConcierge"
)
// Concierge interacts with the vm_concierge daemon, which starts, stops, and
// monitors VMs. It also interacts with the cicerone daemon, which interacts
// with containers inside those VMs.
type Concierge struct {
ownerID string // cryptohome hash for the logged-in user
conciergeObj dbus.BusObject
ciceroneObj dbus.BusObject
}
// GetRunningConcierge returns a concierge instance without restarting concierge service.
// Returns an error if concierge is not available.
func GetRunningConcierge(ctx context.Context, user string) (*Concierge, error) {
h, err := cryptohome.UserHash(ctx, user)
if err != nil {
return nil, err
}
// Try to get a connection to a running concierge instance. If it's not available,
// returns with an error immediately.
conn, err := dbusutil.SystemBus()
if err != nil {
return nil, errors.Wrap(err, "failed to connect to system bus")
}
if !dbusutil.ServiceOwned(ctx, conn, conciergeName) {
return nil, errors.Wrapf(err, "%s is not owned", conciergeName)
}
concierge := conn.Object(conciergeName, conciergePath)
cicerone := conn.Object(ciceroneName, ciceronePath)
return &Concierge{h, concierge, cicerone}, nil
}
// NewConcierge restarts the vm_concierge service, which stops all running VMs.
func NewConcierge(ctx context.Context, user string) (*Concierge, error) {
h, err := cryptohome.UserHash(ctx, user)
if err != nil {
return nil, err
}
testing.ContextLogf(ctx, "Restarting %v job", conciergeJob)
if err = upstart.RestartJob(ctx, conciergeJob); err != nil {
return nil, errors.Wrapf(err, "%v Upstart job failed", conciergeJob)
}
bus, concierge, err := dbusutil.Connect(ctx, conciergeName, conciergePath)
if err != nil {
return nil, err
}
cicerone := bus.Object(ciceroneName, ciceronePath)
testing.ContextLogf(ctx, "Restarting %v job", ciceroneJob)
if err = upstart.RestartJob(ctx, ciceroneJob); err != nil {
return nil, errors.Wrapf(err, "%v Upstart job failed", ciceroneJob)
}
if err = dbusutil.WaitForService(ctx, bus, ciceroneName); err != nil {
return nil, errors.Wrapf(err, "%v D-Bus service unavailable", ciceroneName)
}
return &Concierge{h, concierge, cicerone}, nil
}
// StopConcierge stops the vm_concierge service, which stops all running VMs.
func StopConcierge(ctx context.Context) error {
testing.ContextLogf(ctx, "Stopping %v job", conciergeJob)
if err := upstart.StopJob(ctx, conciergeJob); err != nil {
return errors.Wrapf(err, "%v Upstart job failed to stop", conciergeJob)
}
return nil
}
// GetEncodedName returns the encoded version of the user-chosen name which
// concierge uses to identify several pieces of the VM (its stateful image, log
// files, and ssh keys).
func GetEncodedName(name string) string {
return base64.URLEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(name))
}
// GetVMDiskInfo returns a VmDiskInfo proto for the given VM via ListVmDisks
func (c *Concierge) GetVMDiskInfo(ctx context.Context, vmName string) (*vmpb.VmDiskInfo, error) {
resp := &vmpb.ListVmDisksResponse{}
if err := dbusutil.CallProtoMethod(ctx, c.conciergeObj, conciergeInterface+".ListVmDisks",
&vmpb.ListVmDisksRequest{
CryptohomeId: c.ownerID,
AllLocations: true,
VmName: DefaultVMName,
}, resp); err != nil {
return nil, err
}
if !resp.GetSuccess() {
return nil, errors.Errorf("could not fetch VM disks info: %v", resp.GetFailureReason())
}
for _, diskInfo := range resp.GetImages() {
if diskInfo.GetName() == vmName {
return diskInfo, nil
}
}
return nil, errors.Errorf("could not find vm named %v", vmName)
}
// listVMDisksSize returns the size of the named VM through ListVmDisks.
func (c *Concierge) listVMDisksSize(ctx context.Context, vmName string) (size uint64, err error) {
disk, err := c.GetVMDiskInfo(ctx, vmName)
if err != nil {
return 0, err
}
return disk.Size, nil
}
func (c *Concierge) createDiskImage(ctx context.Context, diskSize uint64) (diskPath string, err error) {
resp := &vmpb.CreateDiskImageResponse{}
if err = dbusutil.CallProtoMethod(ctx, c.conciergeObj, conciergeInterface+".CreateDiskImage",
&vmpb.CreateDiskImageRequest{
CryptohomeId: c.ownerID,
VmName: DefaultVMName,
DiskSize: diskSize,
ImageType: vmpb.DiskImageType_DISK_IMAGE_AUTO,
StorageLocation: vmpb.StorageLocation_STORAGE_CRYPTOHOME_ROOT,
}, resp); err != nil {
return "", err
}
diskStatus := resp.GetStatus()
if diskStatus != vmpb.DiskImageStatus_DISK_STATUS_CREATED &&
diskStatus != vmpb.DiskImageStatus_DISK_STATUS_EXISTS {
return "", errors.Errorf("could not create disk image: %v", resp.GetFailureReason())
}
return resp.GetDiskPath(), nil
}
// SyncTimes runs the SyncVmTimes dbus method in concierge.
func (c *Concierge) SyncTimes(ctx context.Context) error {
resp := &vmpb.SyncVmTimesResponse{}
if err := dbusutil.CallProtoMethod(ctx, c.conciergeObj, conciergeInterface+".SyncVmTimes",
nil, resp); err != nil {
return err
}
failures := resp.GetFailures()
if failures != 0 {
return errors.Errorf("could not set %d (out of %d) times: %v", resp.GetFailures(), resp.GetRequests(), resp.GetFailureReason())
}
return nil
}
func (c *Concierge) startTerminaVM(ctx context.Context, vm *VM) (string, error) {
// Create the new disk first.
diskPath, err := c.createDiskImage(ctx, vm.targetDiskSize)
if err != nil {
return diskPath, err
}
tremplin, err := dbusutil.NewSignalWatcherForSystemBus(ctx, dbusutil.MatchSpec{
Type: "signal",
Path: ciceronePath,
Interface: ciceroneInterface,
Member: "TremplinStarted",
})
defer tremplin.Close(ctx)
// Get the number of online cpus.
buf, err := ioutil.ReadFile("/sys/devices/system/cpu/online")
if err != nil {
return diskPath, errors.Wrap(err, "failed to read number of online cpus")
}
cpus := uint32(0)
for _, s := range strings.Split(string(buf), ",") {
// First try to see if it is a range of values.
var first, last uint32
if _, err := fmt.Sscanf(s, "%v-%v", &first, &last); err == nil {
cpus += (last - first) + 1
continue
}
// It's not a range of values so try to parse it like a single value.
var val uint32
if _, err := fmt.Sscanf(s, "%v", &val); err != nil {
return diskPath, errors.Wrap(err, "failed to parse online cpu value")
}
cpus++
}
resp := &vmpb.StartVmResponse{}
if err = dbusutil.CallProtoMethod(ctx, c.conciergeObj, conciergeInterface+".StartVm",
&vmpb.StartVmRequest{
Name: vm.name,
StartTermina: true,
OwnerId: c.ownerID,
Disks: []*vmpb.DiskImage{
{
Path: diskPath,
ImageType: vmpb.DiskImageType_DISK_IMAGE_AUTO,
Writable: true,
DoMount: false,
},
},
EnableGpu: vm.EnableGPU,
Cpus: cpus,
}, resp); err != nil {
return diskPath, err
}
if !resp.GetSuccess() {
return diskPath, errors.Errorf("failed to start VM: %s", resp.GetFailureReason())
}
testing.ContextLog(ctx, "Waiting for TremplinStarted D-Bus signal")
sigResult := &cpb.TremplinStartedSignal{}
select {
case sig := <-tremplin.Signals:
if len(sig.Body) == 0 {
return diskPath, errors.New("TremplinStarted signal lacked a body")
}
buf, ok := sig.Body[0].([]byte)
if !ok {
return diskPath, errors.New("TremplinStarted signal body is not a byte slice")
}
if err := proto.Unmarshal(buf, sigResult); err != nil {
return diskPath, errors.Wrap(err, "failed unmarshaling TremplinStarted body")
}
case <-ctx.Done():
return diskPath, errors.Wrap(ctx.Err(), "didn't get TremplinStarted D-Bus signal")
}
if sigResult.OwnerId != c.ownerID {
return diskPath, errors.Errorf("expected owner id %q, received %q", c.ownerID, sigResult.OwnerId)
}
if sigResult.VmName != vm.name {
return diskPath, errors.Errorf("expected VM name %q, received %q", vm.name, sigResult.VmName)
}
vm.ContextID = resp.VmInfo.Cid
vm.seneschalHandle = resp.VmInfo.SeneschalServerHandle
testing.ContextLogf(ctx, "Started VM %q with CID %d and PID %d", vm.name, resp.VmInfo.Cid, resp.VmInfo.Pid)
return diskPath, nil
}
func (c *Concierge) stopVM(ctx context.Context, vm *VM) error {
resp := &vmpb.StopVmResponse{}
if err := dbusutil.CallProtoMethod(ctx, vm.Concierge.conciergeObj, conciergeInterface+".StopVm",
&vmpb.StopVmRequest{
Name: vm.name,
OwnerId: vm.Concierge.ownerID,
}, resp); err != nil {
return err
}
if !resp.GetSuccess() {
return errors.Errorf("failed to stop VM: %v", resp.GetFailureReason())
}
testing.ContextLogf(ctx, "Shut down VM %q", vm.name)
return nil
}
func (c *Concierge) getVMInfo(ctx context.Context, vm *VM) error {
resp := &vmpb.GetVmInfoResponse{}
if err := dbusutil.CallProtoMethod(ctx, vm.Concierge.conciergeObj, conciergeInterface+".GetVmInfo",
&vmpb.GetVmInfoRequest{
Name: vm.name,
OwnerId: vm.Concierge.ownerID,
}, resp); err != nil {
return err
}
if !resp.GetSuccess() {
return errors.New("failed to get VM info")
}
vm.ContextID = resp.VmInfo.Cid
vm.seneschalHandle = resp.VmInfo.SeneschalServerHandle
return nil
}
// GetOwnerID returns the cryptohome hash for the logged-in user.
func (c *Concierge) GetOwnerID() string {
return c.ownerID
}