blob: 8c9f6118d0ac0b5363da1c3ad912ce39c1b49bb7 [file] [log] [blame]
package impl
// The device invoker is responsible for managing the state of the device
// manager itself. The implementation expects that the device manager
// installations are all organized in the following directory structure:
//
// <config.Root>/
// device-manager/
// info - metadata for the device manager (such as object
// name and process id)
// <version 1 timestamp>/ - timestamp of when the version was downloaded
// deviced - the device manager binary
// deviced.sh - a shell script to start the binary
// <version 2 timestamp>
// ...
// device-data/
// acl.devicemanager
// acl.signature
// associated.accounts
//
// The device manager is always expected to be started through the symbolic link
// passed in as config.CurrentLink, which is monitored by an init daemon. This
// provides for simple and robust updates.
//
// To update the device manager to a newer version, a new workspace is created
// and the symlink is updated to the new deviced.sh script. Similarly, to revert
// the device manager to a previous version, all that is required is to update
// the symlink to point to the previous deviced.sh script.
import (
"bufio"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"sync"
"time"
"v.io/core/veyron2"
"v.io/core/veyron2/context"
"v.io/core/veyron2/ipc"
"v.io/core/veyron2/mgmt"
"v.io/core/veyron2/naming"
"v.io/core/veyron2/services/mgmt/application"
"v.io/core/veyron2/services/mgmt/binary"
"v.io/core/veyron2/services/mgmt/device"
"v.io/core/veyron2/services/security/access"
"v.io/core/veyron2/verror2"
"v.io/core/veyron2/vlog"
vexec "v.io/core/veyron/lib/exec"
"v.io/core/veyron/lib/netstate"
"v.io/core/veyron/services/mgmt/device/config"
"v.io/core/veyron/services/mgmt/profile"
)
type updatingState struct {
// updating is a flag that records whether this instance of device
// manager is being updated.
updating bool
// updatingMutex is a lock for coordinating concurrent access to
// <updating>.
updatingMutex sync.Mutex
}
func newUpdatingState() *updatingState {
return new(updatingState)
}
func (u *updatingState) testAndSetUpdating() bool {
u.updatingMutex.Lock()
defer u.updatingMutex.Unlock()
if u.updating {
return true
}
u.updating = true
return false
}
func (u *updatingState) unsetUpdating() {
u.updatingMutex.Lock()
u.updating = false
u.updatingMutex.Unlock()
}
// deviceService implements the Device manager's Device interface.
type deviceService struct {
updating *updatingState
stopHandler func()
callback *callbackState
config *config.State
disp *dispatcher
uat BlessingSystemAssociationStore
}
// managerInfo holds state about a running device manager.
type managerInfo struct {
MgrName string
Pid int
}
func saveManagerInfo(dir string, info *managerInfo) error {
jsonInfo, err := json.Marshal(info)
if err != nil {
vlog.Errorf("Marshal(%v) failed: %v", info, err)
return verror2.Make(ErrOperationFailed, nil)
}
if err := os.MkdirAll(dir, os.FileMode(0700)); err != nil {
vlog.Errorf("MkdirAll(%v) failed: %v", dir, err)
return verror2.Make(ErrOperationFailed, nil)
}
infoPath := filepath.Join(dir, "info")
if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil {
vlog.Errorf("WriteFile(%v) failed: %v", infoPath, err)
return verror2.Make(ErrOperationFailed, nil)
}
return nil
}
func loadManagerInfo(dir string) (*managerInfo, error) {
infoPath := filepath.Join(dir, "info")
info := new(managerInfo)
if infoBytes, err := ioutil.ReadFile(infoPath); err != nil {
vlog.Errorf("ReadFile(%v) failed: %v", infoPath, err)
return nil, verror2.Make(ErrOperationFailed, nil)
} else if err := json.Unmarshal(infoBytes, info); err != nil {
vlog.Errorf("Unmarshal(%v) failed: %v", infoBytes, err)
return nil, verror2.Make(ErrOperationFailed, nil)
}
return info, nil
}
func (s *deviceService) Claim(ctx ipc.ServerContext) error {
return s.disp.claimDeviceManager(ctx)
}
func (*deviceService) Describe(ipc.ServerContext) (device.Description, error) {
empty := device.Description{}
deviceProfile, err := computeDeviceProfile()
if err != nil {
return empty, err
}
knownProfiles, err := getKnownProfiles()
if err != nil {
return empty, err
}
result := matchProfiles(deviceProfile, knownProfiles)
return result, nil
}
func (*deviceService) IsRunnable(_ ipc.ServerContext, description binary.Description) (bool, error) {
deviceProfile, err := computeDeviceProfile()
if err != nil {
return false, err
}
binaryProfiles := make([]profile.Specification, 0)
for name, _ := range description.Profiles {
profile, err := getProfile(name)
if err != nil {
return false, err
}
binaryProfiles = append(binaryProfiles, *profile)
}
result := matchProfiles(deviceProfile, binaryProfiles)
return len(result.Profiles) > 0, nil
}
func (*deviceService) Reset(call ipc.ServerContext, deadline uint64) error {
// TODO(jsimsa): Implement.
return nil
}
// getCurrentFileInfo returns the os.FileInfo for both the symbolic link
// CurrentLink, and the device script in the workspace that this link points to.
func (s *deviceService) getCurrentFileInfo() (os.FileInfo, string, error) {
path := s.config.CurrentLink
link, err := os.Lstat(path)
if err != nil {
vlog.Errorf("Lstat(%v) failed: %v", path, err)
return nil, "", err
}
scriptPath, err := filepath.EvalSymlinks(path)
if err != nil {
vlog.Errorf("EvalSymlinks(%v) failed: %v", path, err)
return nil, "", err
}
return link, scriptPath, nil
}
func (s *deviceService) revertDeviceManager(ctx *context.T) error {
if err := updateLink(s.config.Previous, s.config.CurrentLink); err != nil {
return err
}
runtime := veyron2.RuntimeFromContext(ctx)
runtime.AppCycle().Stop()
return nil
}
func (s *deviceService) newLogfile(prefix string) (*os.File, error) {
d := filepath.Join(s.config.Root, "device_test_logs")
if _, err := os.Stat(d); err != nil {
if err := os.MkdirAll(d, 0700); err != nil {
return nil, err
}
}
f, err := ioutil.TempFile(d, "__device_impl_test__"+prefix)
if err != nil {
return nil, err
}
return f, nil
}
// TODO(cnicolaou): would this be better implemented using the modules
// framework now that it exists?
func (s *deviceService) testDeviceManager(ctx *context.T, workspace string, envelope *application.Envelope) error {
path := filepath.Join(workspace, "deviced.sh")
cmd := exec.Command(path)
for k, v := range map[string]*io.Writer{
"stdout": &cmd.Stdout,
"stderr": &cmd.Stderr,
} {
// Using a log file makes it less likely that stdout and stderr
// output will be lost if the child crashes.
file, err := s.newLogfile(fmt.Sprintf("deviced-test-%s", k))
if err != nil {
return err
}
fName := file.Name()
defer os.Remove(fName)
*v = file
defer func() {
if f, err := os.Open(fName); err == nil {
scanner := bufio.NewScanner(f)
for scanner.Scan() {
vlog.Infof("[testDeviceManager %s] %s", k, scanner.Text())
}
}
}()
}
// Setup up the child process callback.
callbackState := s.callback
listener := callbackState.listenFor(mgmt.ChildNameConfigKey)
defer listener.cleanup()
cfg := vexec.NewConfig()
cfg.Set(mgmt.ParentNameConfigKey, listener.name())
cfg.Set(mgmt.ProtocolConfigKey, "tcp")
cfg.Set(mgmt.AddressConfigKey, "127.0.0.1:0")
handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
// Start the child process.
if err := handle.Start(); err != nil {
vlog.Errorf("Start() failed: %v", err)
return verror2.Make(ErrOperationFailed, ctx)
}
defer func() {
if err := handle.Clean(); err != nil {
vlog.Errorf("Clean() failed: %v", err)
}
}()
// Wait for the child process to start.
if err := handle.WaitForReady(childReadyTimeout); err != nil {
vlog.Errorf("WaitForReady(%v) failed: %v", childReadyTimeout, err)
return verror2.Make(ErrOperationFailed, ctx)
}
childName, err := listener.waitForValue(childReadyTimeout)
if err != nil {
return verror2.Make(ErrOperationFailed, ctx)
}
// Check that invoking Revert() succeeds.
childName = naming.Join(childName, "device")
dmClient := device.DeviceClient(childName)
linkOld, pathOld, err := s.getCurrentFileInfo()
if err != nil {
return verror2.Make(ErrOperationFailed, ctx)
}
// Since the resolution of mtime for files is seconds, the test sleeps
// for a second to make sure it can check whether the current symlink is
// updated.
time.Sleep(time.Second)
if err := dmClient.Revert(ctx); err != nil {
return verror2.Make(ErrOperationFailed, ctx)
}
linkNew, pathNew, err := s.getCurrentFileInfo()
if err != nil {
return verror2.Make(ErrOperationFailed, ctx)
}
// Check that the new device manager updated the current symbolic link.
if !linkOld.ModTime().Before(linkNew.ModTime()) {
vlog.Errorf("New device manager test failed")
return verror2.Make(ErrOperationFailed, ctx)
}
// Ensure that the current symbolic link points to the same script.
if pathNew != pathOld {
updateLink(pathOld, s.config.CurrentLink)
vlog.Errorf("New device manager test failed")
return verror2.Make(ErrOperationFailed, ctx)
}
if err := handle.Wait(childWaitTimeout); err != nil {
vlog.Errorf("New device manager failed to exit cleanly: %v", err)
return verror2.Make(ErrOperationFailed, ctx)
}
return nil
}
// TODO(caprita): Move this to util.go since device_installer is also using it now.
func generateScript(workspace string, configSettings []string, envelope *application.Envelope) error {
// TODO(caprita): Remove this snippet of code, it doesn't seem to serve
// any purpose.
path, err := filepath.EvalSymlinks(os.Args[0])
if err != nil {
vlog.Errorf("EvalSymlinks(%v) failed: %v", os.Args[0], err)
return verror2.Make(ErrOperationFailed, nil)
}
output := "#!/bin/bash\n"
output += strings.Join(config.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
// Escape the path to the binary; %q uses Go-syntax escaping, but it's
// close enough to Bash that we're using it as an approximation.
//
// TODO(caprita/rthellend): expose and use shellEscape (from
// veyron/tools/debug/impl.go) instead.
output += fmt.Sprintf("exec %q", filepath.Join(workspace, "deviced")) + " "
output += strings.Join(envelope.Args, " ")
output += "\n"
path = filepath.Join(workspace, "deviced.sh")
if err := ioutil.WriteFile(path, []byte(output), 0700); err != nil {
vlog.Errorf("WriteFile(%v) failed: %v", path, err)
return verror2.Make(ErrOperationFailed, nil)
}
return nil
}
func (s *deviceService) updateDeviceManager(ctx *context.T) error {
if len(s.config.Origin) == 0 {
return verror2.Make(ErrUpdateNoOp, ctx)
}
envelope, err := fetchEnvelope(ctx, s.config.Origin)
if err != nil {
return err
}
if envelope.Title != application.DeviceManagerTitle {
return verror2.Make(ErrAppTitleMismatch, ctx)
}
if s.config.Envelope != nil && reflect.DeepEqual(envelope, s.config.Envelope) {
return verror2.Make(ErrUpdateNoOp, ctx)
}
// Create new workspace.
workspace := filepath.Join(s.config.Root, "device-manager", generateVersionDirName())
perm := os.FileMode(0700)
if err := os.MkdirAll(workspace, perm); err != nil {
vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
return verror2.Make(ErrOperationFailed, ctx)
}
deferrer := func() {
cleanupDir(workspace, "")
}
defer func() {
if deferrer != nil {
deferrer()
}
}()
// Populate the new workspace with a device manager binary.
// TODO(caprita): match identical binaries on binary metadata
// rather than binary object name.
sameBinary := s.config.Envelope != nil && envelope.Binary == s.config.Envelope.Binary
if sameBinary {
if err := linkSelf(workspace, "deviced"); err != nil {
return err
}
} else {
if err := downloadBinary(ctx, workspace, "deviced", envelope.Binary); err != nil {
return err
}
}
// Populate the new workspace with a device manager script.
configSettings, err := s.config.Save(envelope)
if err != nil {
return verror2.Make(ErrOperationFailed, ctx)
}
if err := generateScript(workspace, configSettings, envelope); err != nil {
return err
}
if err := s.testDeviceManager(ctx, workspace, envelope); err != nil {
return err
}
if err := updateLink(filepath.Join(workspace, "deviced.sh"), s.config.CurrentLink); err != nil {
return err
}
runtime := veyron2.RuntimeFromContext(ctx)
runtime.AppCycle().Stop()
deferrer = nil
return nil
}
func (*deviceService) Install(ctx ipc.ServerContext, _ string) (string, error) {
return "", verror2.Make(ErrInvalidSuffix, ctx.Context())
}
func (*deviceService) Refresh(ipc.ServerContext) error {
// TODO(jsimsa): Implement.
return nil
}
func (*deviceService) Restart(ipc.ServerContext) error {
// TODO(jsimsa): Implement.
return nil
}
func (*deviceService) Resume(ctx ipc.ServerContext) error {
return verror2.Make(ErrInvalidSuffix, ctx.Context())
}
func (s *deviceService) Revert(call ipc.ServerContext) error {
if s.config.Previous == "" {
return verror2.Make(ErrUpdateNoOp, call.Context())
}
updatingState := s.updating
if updatingState.testAndSetUpdating() {
return verror2.Make(ErrOperationInProgress, call.Context())
}
err := s.revertDeviceManager(call.Context())
if err != nil {
updatingState.unsetUpdating()
}
return err
}
func (*deviceService) Start(ctx ipc.ServerContext) ([]string, error) {
return nil, verror2.Make(ErrInvalidSuffix, ctx.Context())
}
func (s *deviceService) Stop(call ipc.ServerContext, _ uint32) error {
runtime := veyron2.RuntimeFromContext(call.Context())
if s.stopHandler != nil {
s.stopHandler()
}
runtime.AppCycle().Stop()
return nil
}
func (*deviceService) Suspend(call ipc.ServerContext) error {
runtime := veyron2.RuntimeFromContext(call.Context())
runtime.AppCycle().Stop()
return nil
}
func (*deviceService) Uninstall(ctx ipc.ServerContext) error {
return verror2.Make(ErrInvalidSuffix, ctx.Context())
}
func (s *deviceService) Update(call ipc.ServerContext) error {
ctx, cancel := context.WithTimeout(call.Context(), ipcContextTimeout)
defer cancel()
updatingState := s.updating
if updatingState.testAndSetUpdating() {
return verror2.Make(ErrOperationInProgress, call.Context())
}
err := s.updateDeviceManager(ctx)
if err != nil {
updatingState.unsetUpdating()
}
return err
}
func (*deviceService) UpdateTo(ipc.ServerContext, string) error {
// TODO(jsimsa): Implement.
return nil
}
func (s *deviceService) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
return s.disp.setACL(ctx.LocalPrincipal(), acl, etag, true /* store ACL on disk */)
}
func (s *deviceService) GetACL(_ ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
return s.disp.getACL()
}
func sameMachineCheck(ctx ipc.ServerContext) error {
switch local, err := netstate.SameMachine(ctx.RemoteEndpoint().Addr()); {
case err != nil:
return err
case local == false:
vlog.Errorf("SameMachine() indicates that endpoint is not on the same device")
return verror2.Make(ErrOperationFailed, ctx.Context())
}
return nil
}
// TODO(rjkroege): Make it possible for users on the same system to also
// associate their accounts with their identities.
func (s *deviceService) AssociateAccount(call ipc.ServerContext, identityNames []string, accountName string) error {
if err := sameMachineCheck(call); err != nil {
return err
}
if accountName == "" {
return s.uat.DisassociateSystemAccountForBlessings(identityNames)
} else {
// TODO(rjkroege): Optionally verify here that the required uname is a valid.
return s.uat.AssociateSystemAccountForBlessings(identityNames, accountName)
}
}
func (s *deviceService) ListAssociations(call ipc.ServerContext) (associations []device.Association, err error) {
// Temporary code. Dump this.
vlog.VI(2).Infof("ListAssociations given blessings: %v\n", call.RemoteBlessings().ForContext(call))
if err := sameMachineCheck(call); err != nil {
return nil, err
}
return s.uat.AllBlessingSystemAssociations()
}